泛型概述
泛型是现代编程语言中的重要特性,简单来说就是不必指定类型,可以写出非特定类型,模板化的代码,提高代码重用率。
泛型应用最广的地方应该就是容器类了。在Java的容器类中大量的使用了泛型。
例如ArrayList
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
@java.io.Serial
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
可以看到ArrayList存放数据本质就是一个Object数组elementData,因此,如果不使用泛型,直接存储Object。比如将String类型放入容器,那么在get函数取出元素时类型为Object,这时候就需要强制转换。
java
ArrayList list = new ArrayList();
list.add("test");
String str = (String) list.get(0);
如何这个容器中还存在其他类型的元素,那么取出元素时就很容易出现ClassCastException异常。
java
list.add(123);
str = (String) list.get(1);
当然,如果只写一个专门存储String或者Integer的ArrayList也可以,但是这样就需要给每一个类型都写单独编写,更别提还有自己写的类。
因此,泛型就出现了,泛型类可以在编译阶段就检查类型,这样就不会导致类型转换的异常。
下面是一个最简单的泛型类
java
public class Generic<T>{
private T val;
public Generic(T val) {
this.val = val;
}
public T getVal(){
return val;
}
}
泛型擦除
泛型擦除是指Java中的泛型只在编译期有效,在运行期间会被删除。
如下面这段代码
java
public class Foo {
public void test(List<String> stringList){
}
public void test(List<Integer> integerList) {
}
}
这段代码会报错,方法不能重载,原因就是上面两个方法,在编译后被泛型擦除,最后都是
java
public void test(List) {}
因此不能区分两个函数。
泛型类的继承
泛型类的继承关系不是由泛型类型决定的,如List<Integer>和List<Number>,虽然Integer继承自Number,但是List<Integer>和List<Number>并没有继承关系。
要想使两个泛型类具有继承关系,只能使两个泛型类本身之间继承,或实现接口。
如上面的ArrayList就继承了AbstractList<E<以及List<E<接口。
泛型的逆变和协变
先从一个数组说起,Java的数组是协变的。
看下面这段代码
java
public class Test {
public static void main(String[] args) {
Number[] arr = new Integer[2];
arr[0] = 1;
arr[1] = 0.5;
}
}
这段代码在编译器并不会出错,但是一旦运行,将会抛出一个异常
javastacktrace
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double
at Test.main(Test.java:5)
这是因为Integer是Number的子类型,因此Integer[]也是Number[]的子类型,这样的性质被称为协变,在编译器并没有检查出错误。
但是在运行时,jvm虚拟机发现这个arr其实是一个Integer类型的数组,不是Number类型,所以不能存放进入double类型的数字,因此抛出了一个异常。
泛型的不变性
因此,在吸取了上面的教训之后,泛型被设计为不变,也就是说,List<Integer>并不是List<Number>的子类型。
这样在编译器就可以检查出错误,防止运行期再报错。
但是这样就引入一个新的问题,如何才能实现协变呢。
协变在Java中还是很常用的,比如我只想要一个Fruit集合,里面存放着水果,但我不想管里面到底存放的是哪种水果。
java
public void consume(List<Fruit> list) {
......
}
这时,泛型的不变性就带来了麻烦,加入我现在有一个List<Apple>,因为List<Fruit>并不是List<Apple>的父类型,参数就传递不进去。
java
List<Apple> appleList = new ArrayList<Apple>;
consume(appleList); // 报错
因此,在泛型中如何实现协变就成为了一个问题。
还有一种情况,如果我们希望往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
java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
在这里,src使用extends进行协变,只可读取,dest使用super进行逆变,保证写入的类型检查。