泛型概述
泛型是现代编程语言中的重要特性,简单来说就是不必指定类型,可以写出非特定类型,模板化的代码,提高代码重用率。
泛型应用最广的地方应该就是容器类了。在Java的容器类中大量的使用了泛型。
例如ArrayList
1 | public class ArrayList<E> extends AbstractList<E> |
可以看到ArrayList存放数据本质就是一个Object数组elementData,因此,如果不使用泛型,直接存储Object。比如将String类型放入容器,那么在get函数取出元素时类型为Object,这时候就需要强制转换。
1 | ArrayList list = new ArrayList(); |
如何这个容器中还存在其他类型的元素,那么取出元素时就很容易出现ClassCastException异常。
1 | list.add(123); |
当然,如果只写一个专门存储String或者Integer的ArrayList也可以,但是这样就需要给每一个类型都写单独编写,更别提还有自己写的类。
因此,泛型就出现了,泛型类可以在编译阶段就检查类型,这样就不会导致类型转换的异常。
下面是一个最简单的泛型类
1 | public class Generic<T>{ |
泛型擦除
泛型擦除是指Java中的泛型只在编译期有效,在运行期间会被删除。
如下面这段代码
1 | public class Foo { |
这段代码会报错,方法不能重载,原因就是上面两个方法,在编译后被泛型擦除,最后都是
1 | public void test(List) {} |
因此不能区分两个函数。
泛型类的继承
泛型类的继承关系不是由泛型类型决定的,如List<Integer>和List<Number>,虽然Integer继承自Number,但是List<Integer>和List<Number>并没有继承关系。
要想使两个泛型类具有继承关系,只能使两个泛型类本身之间继承,或实现接口。
如上面的ArrayList就继承了AbstractList<E<以及List<E<接口。
泛型的逆变和协变
先从一个数组说起,Java的数组是协变的。
看下面这段代码
1 | public class Test { |
这段代码在编译器并不会出错,但是一旦运行,将会抛出一个异常
1 | Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double |
这是因为Integer是Number的子类型,因此Integer[]也是Number[]的子类型,这样的性质被称为协变,在编译器并没有检查出错误。
但是在运行时,jvm虚拟机发现这个arr其实是一个Integer类型的数组,不是Number类型,所以不能存放进入double类型的数字,因此抛出了一个异常。
泛型的不变性
因此,在吸取了上面的教训之后,泛型被设计为不变,也就是说,List<Integer>并不是List<Number>的子类型。
这样在编译器就可以检查出错误,防止运行期再报错。
但是这样就引入一个新的问题,如何才能实现协变呢。
协变在Java中还是很常用的,比如我只想要一个Fruit集合,里面存放着水果,但我不想管里面到底存放的是哪种水果。
1 | public void consume(List<Fruit> list) { |
这时,泛型的不变性就带来了麻烦,加入我现在有一个List<Apple>,因为List<Fruit>并不是List<Apple>的父类型,参数就传递不进去。
1 | List<Apple> appleList = new ArrayList<Apple>; |
因此,在泛型中如何实现协变就成为了一个问题。
还有一种情况,如果我们希望往List<Object>中放水果,使用一个produce函数将所有List<Apple>或者List<Banana>的元素全部添加到List<Object>,但又希望在produce函数中向容器添加非Fruit的其他元素时进行检查并报错,这时候就需要逆变。
泛型通配符
要实现泛型协变和逆变,这时通配符 ? 就派上用场了。
<? extends>
实现了泛型的协变<? super>
实现了泛型的逆变
在上面的代码中,假如在consume函数中我们想传入参数,就需要把List<Fruit>改为List<? extends Fruit>。这样就不会产生报错了。
List<? extends Fruit>,其中<? extends Fruit>代表的类型为:Fruit及其子类型,此时传入List<Apple>就没有问题了。
但是当List<Apple>协变为List<? extends Fruit>之后,就不能往容器中再放入元素了。
原因在于,当容器协变后,List<? extends Fruit>中的类型不能再被确定为Apple,<? extends Fruit>虽然包含Apple,但是并不特指为Apple。因此,如果放入一个其他的类型,比如Banana,那么在使用上一个List<Apple>进行读取的时候就会出现类型转换错误。
同样的,如果希望往Fruit中放水果,就可以使用<? super Fruit>让List<Object>逆变为List<? super Fruit>,这样在函数中就可以调用add方法。
从上面的例子可以看出,extends
确定了泛型的上界,而super
确定了泛型的下界。
PECS
究竟什么时候使用extends,什么时候使用super。也就是PECS
PECS: producer-extends, consumer-super.
生产者使用extends,因为协变只可读取,不可写入。消费者使用super,因为super写入可以保证类型检查。
在Collections中的copy函数就很好地诠释了PECS
1 | public static <T> void copy(List<? super T> dest, List<? extends T> src) { |
在这里,src使用extends进行协变,只可读取,dest使用super进行逆变,保证写入的类型检查。