0%

Java泛型

泛型概述

泛型是现代编程语言中的重要特性,简单来说就是不必指定类型,可以写出非特定类型,模板化的代码,提高代码重用率。

泛型应用最广的地方应该就是容器类了。在Java的容器类中大量的使用了泛型。

例如ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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,这时候就需要强制转换。

1
2
3
ArrayList list = new ArrayList();
list.add("test");
String str = (String) list.get(0);

如何这个容器中还存在其他类型的元素,那么取出元素时就很容易出现ClassCastException异常。

1
2
list.add(123);
str = (String) list.get(1);

当然,如果只写一个专门存储String或者Integer的ArrayList也可以,但是这样就需要给每一个类型都写单独编写,更别提还有自己写的类。

因此,泛型就出现了,泛型类可以在编译阶段就检查类型,这样就不会导致类型转换的异常。

下面是一个最简单的泛型类

1
2
3
4
5
6
7
8
9
10
11
public class Generic<T>{ 
private T val;

public Generic(T val) {
this.val = val;
}

public T getVal(){
return val;
}
}

泛型擦除

泛型擦除是指Java中的泛型只在编译期有效,在运行期间会被删除。

如下面这段代码

1
2
3
4
5
6
7
8
public class Foo {  
public void test(List<String> stringList){

}
public void test(List<Integer> integerList) {

}
}

这段代码会报错,方法不能重载,原因就是上面两个方法,在编译后被泛型擦除,最后都是

1
public void test(List) {}

因此不能区分两个函数。

泛型类的继承

泛型类的继承关系不是由泛型类型决定的,如List<Integer>和List<Number>,虽然Integer继承自Number,但是List<Integer>和List<Number>并没有继承关系。

要想使两个泛型类具有继承关系,只能使两个泛型类本身之间继承,或实现接口。

如上面的ArrayList就继承了AbstractList<E<以及List<E<接口。

泛型的逆变和协变

先从一个数组说起,Java的数组是协变的。

看下面这段代码

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
Number[] arr = new Integer[2];
arr[0] = 1;
arr[1] = 0.5;
}
}

这段代码在编译器并不会出错,但是一旦运行,将会抛出一个异常

1
2
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集合,里面存放着水果,但我不想管里面到底存放的是哪种水果。

1
2
3
public void consume(List<Fruit> list) {
......
}

这时,泛型的不变性就带来了麻烦,加入我现在有一个List<Apple>,因为List<Fruit>并不是List<Apple>的父类型,参数就传递不进去。

1
2
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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进行逆变,保证写入的类型检查。