2460 2017-06-10 2020-06-25

前言:对于Java容器载体来说,容器要具有通用性,即不能限制为某个类型的容器。这时,泛型出现了。

一、泛型

1、概述

泛型的出现似乎就是为了完善容器类库的,我们来看下代码

  • 最原始的容器类,持有确定类对象
public class Holder1() {
    private Test test;
    public Holder1(Test test) {
        this.test = test;
    }
    public Test getTest() {
        return test;
    }
}
  • 向上转型的容器类,强制转为目标类型
public class Holder2() {
    private Object obj;
    public Holder2(Object obj) {
        this.obj = obj;
    }
    public Object getObj() {
        return obj;
    }
    public static void main(String[] hk) {
        String str = new String("hk");
        Holder2 holder2 = new Holder2(str);
        String a = (String)holder2.getObj();
        //Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
        Integer b = (Integer)holder2.getObj();
    }
}
  • 下面就是泛型了,知道确切对象类型
//当然,也可以这么用:Holder2<A, b, CD, HK>
public class Holder2<HK> {
    private HK obj;
    public Holder2(HK obj) {
        this.obj = obj;
    }
    public Object getObj() {
        return obj;
    }
    public static void main(String[] hk) {
        String str = new String("hk");
        Holder2<String> holder2 = new Holder2(str);
        System.out.println(holder2.getObj());
    }
}

通过上面的代码,泛型的优点就稍稍突显出来了,我们往下看。

2、泛型方法

没错,不仅可以在类结构层次添加泛型,方法层次也支持泛型

  • 无返回类型
public <A, B> void get1(A a, B b) {
    System.out.println(a.getClass().getName());
}
  • 有返回类型,这种情况下需要一个容器载体,因为泛型不能单独存在
public <A, B> void get1(A a, B b) {
    return new HashMap<A, B>();
}

二、类型擦除

1、擦除

我们看下下面的两段代码

  • 第一段
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
//说明无法从泛型内部发现任何有关泛型参数的类型信息
System.out.println(c1 == c2);//true
  • 第二段
class HasF {
    public void f() {
        System.out.println("HasF.f()");
    }
}
class Manipulator<T> {
    private T obj;
    public Manipulator(T t) {
        obj = t;
    }
    public void manipulate() {
        //说明原类型被擦除,即无法从泛型内部发现任何有关泛型参数的类型信息
        //编译不通过 obj.f();
    }
}
public class Manipulation {
    public static void main(String hk) {
        HasF hasF = new HasF();
        Manipulator<HasF> manipulator = new Manipulator<>(hasF);
        manipulator.manipulate();
    }
}

通过上面的两段代码,我们知道,被声明泛型的参数的容器,再未明确指定泛型类型前,是无法获取任何有关泛型参数的类型信息,这是因为在JVM运行时把泛型向上转为了Object(List< E >则被转换为原生的List),而当明确指定泛型参数的类型后,JVM再把Object类向下转为目标参数类型,如下

//new Manipulator<>(hasF)在编译时,等同于new Manipulator<Object>(hasF),前面的代码代表了向下转换的指令
Manipulator<HasF> manipulator = new Manipulator<>(hasF);

2、擦除补偿

擦除机制显然是存在问题的,但也可以在代码层次进行补偿,或者说防止跳坑。

  • Sun建议的显式工厂方式,代码如下
public class FactoryConstraint {
    public static void main(String[] hk) {
        new Foo2<Integer>(new IntegerFactory());
        new Foo2<Widget>(new Widget.Factory());
    }
}
interface FactoryI<T> {
    T create();
}
class Foo2<T> {
    private T x;
    public <F extends FactoryI<T>> Foo2(F factory) {
        x = factory.create();
    }
}
class IntegerFactory implements FactoryI<Integer> {
    public Integer create() {
        return new Integer(0);
    }
}
class Widget {
    public static class Factory implements FactoryI<Widget> {
        public Widget create() {
            return new Widget();
        }
    }
}

通过这种方式可以获得编译时期的类型检查,下同。

  • 另外一种是模板方法,代码如下
public class CreatorGeneric {
    public static void main(String[] hk) {
        Creator c = new Creator();
        c.f();
    }
}
abstract class GenericWithCreate<T> {
    final T element;
    GenericWithCreate() {
        element = create();
    }
    abstract T create();
}
class X{}
class Creator extends GenericWithCreate<X> {
    X create() {
        return new X();
    }
    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}

小结:上面两种都是通过设计模式来对Java泛型的类型擦除进行补救,对于具体的设计模式,这里不做深究,了解就行了。

三、泛型数组

1、概述

我们先看下下面的代码,如下

  • 第一段
class<T[]> Test{}//错误语法,泛型不能定义为数组 
  • 第二段
class TestClass<T>{}
public class Test {
    public static void main(String[] hk) {
//        TestClass<String>[] test = new TestClass<String>[1];//语法错误,不能创建泛型数组
        TestClass<String>[] test1 = new TestClass[1];//语法通过,可能有点不理解,往下看
        System.out.println(test1.length);//1
        test1[0] = new TestClass<String>();
        System.out.println(test1[0] == null);//false,说明泛型参数不起作用,本质还是TestClass数组
        ArrayList<String>[] ss = new ArrayList[10];
        System.out.println(ss.length);//为10
    }
}

通过上面我们知道了,Java里面是不支持泛型数组的实例化(虽然可以定义)。问题在于数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的。因此,上面的所谓泛型数组,将忽略泛型参数,从而变为数组。成功创建泛型数组的唯一方式,不是实例化一个泛型数组,而是先创建一个被擦除类型的新数组,再对其转型。如下

  • 第三段
class Generic<T>{} 
public class ArrayOfGeneric {

    static final int SIZE = 100;
    static Generic<Integer>[] gia;

    public static void main(String[] args) {
        //gia = (Generic<Integer>[])new Object[SIZE];运行抛出异常
        gia = (Generic<Integer>[]) new Generic[SIZE];
        System.out.println(gia.getClass().getSimpleName());
        gia[0] = new Generic<Integer>();
        //gia[0] = new Generic<Object>();编译不通过
        //gia[1] = new Object();编译不通过
        //gia[2] = new Generic<Double>();编译不通过
        //Double d = (Double)gia[1];编译不通过
        //Object dd = (Object)gia[1];编译通过
    }
}

在Java中,数组会追踪数组对象的实际类型,这意味着数组内部的元素不能通过任何方式的转型进入进行(Object类也不行),但可以转型转出去。

2、T[] 数组

我们看一下稍稍复杂一点的代码

public class GenericArray<T> {
    private T[] array;
    public GenericArray(int size) {
        array = (T[]) new Object[size];
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) {
        return array[index];
    }
    public T[] rep() {
        return array;
    }
    public static void main(String[] args) {
        GenericArray<Integer> gai = new GenericArray<>(10);
        Object[] oa = gai.rep();
        //ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer
        //Integer[] ia = gai.rep();
    }
}

常规来讲rep返回的就是Integer[]数组,但结果却不是。这是因为擦除机制,即泛型数组的运行时类型只能是Object[]数组,不能向下转。如果我们立即将其转型为T[]数组,如Integer[] ia = gai.rep()那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。正因为这样,最好在集合内部使用Object[]数组,取出使用时,再将对Object[]转为T[]

3、类型标记

这是一种新的方式,稍稍改了一下上面的代码,我们看下

public class GenericArrayWithTypeToken<T> {
    private T[] array;
    public GenericArrayWithTypeToken(Class<T> type, int size) {
        //类型标记Class<T>被传递到构造器中,以便从擦除中恢复,使得我们可以得到目标数组
        array = (T[]) Array.newInstance(type, size);
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) {
        return array[index];
    }
    public T[] rep() {
        return array;
    }
    public static void main(String[] hk) {
        GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
        Integer[] ia = gai.rep();//运行正常
    }
}

说了这么多,自己稍稍总结下:忘了有泛型数组这回事,转而对Object[]数组进行转型,感觉就差不多了。

四、边界

这个很好理解,是对泛型参数的一个类型限制,用于限定特定的泛型(以便于调用父泛型中的方法),如下

class AA{}
interface BB{
    void bb();
}
class AB extends AA implements BB {
    @Override
    public void bb() {
        System.out.println("ab");
    }
}
class CC<T extends AA>{}//这里的extends不区分父类和接口
class DD<T extends BB>{}
class EE<T extends AA & BB>{}
//class FF<T extends BB & AA>{}语法错误,&后面只能接接口,而且可以接多个,和implements类似
class GG<T extends EE>{}
public class TestB{
    public static void main(String[] args) {
        //CC<BB> ccc = new CC<BB>();编译不通过
        CC<AA> cc = new CC<AA>();
        CC<AB> ccc = new CC<AB>();
        EE<AB> cccc = new EE<AB>();
    }
}

小结下:

  • extends不区分父类和接口
  • &后面只能接接口,且可以接多个
  • 泛型类型的限制和普通类的限制相似,单继承,多接口

五、通配符

1、协变、逆变

这个了解概念即可,不做深究,代码如下

  • 协变:用窄类型替代宽类型
public class Test {
    public static void main(String[] args) {
        Number num1 = new Integer(0);//Integer extends Number
        Number[] num2 = new Integer[10];//说明数组是协变的
        ArrayList<Number> flist = new ArrayList<Integer>();//不能编译,旨在说明泛型不是协变的
    }
}
  • 逆变:用宽类型覆盖窄类型

在Java中,不允许把子类赋给父类对象,但可以通过通配符模拟实现,见下文。

2、子类型通配符

先看代码

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class GenericsAndCovariance {
    public static void main(String[] args) {
        // 标识为一个Fruit的List,至于具体是什么,并不关心
        List<? extends Fruit> flist = new ArrayList<Apple>();
        // 下面的都不能编译通过
        // flist.add(new Apple());
        // flist.add(new Fruit());
        // flist.add(new Object());
        flist.add(null); // null是可以的
        // 我们可以确定里面的必定是Fruit类型
        Fruit f = flist.get(0);
    }
}

那么问题来了,我这个List<? extends Fruit>什么都不能做,连一个Apple对象都添加不进去,是这样的。它唯一的功能是指向一个继承Fruit类的List,比如指向List<Orange>,并且我们是可以Fruit对象的方法,而Orange对象的方法则被丢失了。

3、父类型通配符

先看代码

public class SuperTypeWildcards {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
        // apples.add(new Fruit()); // 不能添加
    }
}

上面这段代码是可行的。有了上面讲得两个通配符后,我们可以对泛型进行更好的读写操作,如下

public class Collections {
    //数据源是src,里面存储的肯定是T类型或T的子类,目标是dest,里面存储的是T类型或T的子类
    //一个只能读,且读出来的肯定能写进另一个
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i = 0; i < src.size(); i++)
            dest.set(i, src.get(i));
    }
    //完美,不是吗?
}

4、无界通配符

不讲了,看代码

ArrayList<?> list = new ArrayList<String>();
//编译不通过,即不能对list进行add操作
//list.add(new String("123"));
System.out.println(list.size());//0
ArrayList<String> list1 = new ArrayList<>();
list1.add("123");
list = list1;//指向List引用
System.out.println(list.get(0));//123
System.out.println(list1.get(0));//123
总访问次数: 146次, 一般般帅 创建于 2017-06-10, 最后更新于 2020-06-25

进大厂! 欢迎关注微信公众号,第一时间掌握最新动态!