泛型中的 super 和 extends

很多人对 <? extends T><? super T> 这两个泛型通配符的用法不清楚,经常会出现理解无法,下面来看一下到底如何正确的使用这个组合。

<? extends T>

先来看一段代码,以下的这段代码用来遍历一个装有 Integer 数据的 list:

1
2
3
4
5
6
7
8
9
public void printNumberList(List<Integer> integerList) {
for (Integer i : integerList) {
System.out.println(i);
}
}

// 遍历一个 Integer List
List<Integer> integerList = Arrays.asList(1,2,3,4,5,6);
printNumberList(integerList);

如果我还想遍历另外一个装有 Double 数据的 list,我得写另外一段代码:

1
2
3
4
5
6
7
8
9
public void printNumberList(List<Double> doubleList) {
for (Double d : doubleList) {
System.out.println(d);
}
}

// 遍历一个 Double List
List<Double> doubleList = Arrays.asList(1.1,2.2,3.3,4.4,5.5,6.6);
printNumberList(doubleList);

在工作中这样写代码怕是不行吧?很简单就可以想到使用泛型来对这个代码进行优化:

1
2
3
4
5
6
7
8
9
10
public <T> void printNumberGenericsList(List<T> list) {
for (T l : list) {
System.out.println(l);
}
}
// 利用泛型来遍历多种类型的 List
List<Integer> integerList = Arrays.asList(1,2,3,4,5,6);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3, 4.4, 5.5, 6.6);
printNumberGenericsList(integerList);
printNumberGenericsList(doubleList);

代码运行起来了,很好的解决了这个问题。其实还有问题,看下面的代码:

1
2
List<String> strList = Arrays.asList("name1", "name2", "name3");
printNumberGenericsList(strList);

本来这个方法是想用来打印数字类型的 List,而我放进了一个 String 类型的 List,也能够正常的跑起来,对于这个方法没有什么问题,但是比如说后续把这个方法内部实现改了,加上了数字的运算,再传输 String 类型的 List 就会出错了,所以在这里我想只想接受数字类型的 List,代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void printNumberGenericsExtendsList(List<? extends Number> list) {
for (Number l : list) {
System.out.println(l);
}
}
List<Integer> integerList = Arrays.asList(1,2,3,4,5,6);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3, 4.4, 5.5, 6.6);
printNumberGenericsExtendsList(integerList);
printNumberGenericsExtendsList(doubleList);

List<String> strList = Arrays.asList("name1", "name2", "name3");
// 在编译期就无法通过
// printNumberGenericsExtendsList(strList);

现在我们知道了 <? extends T> 的用法,为了将这个问题说的更清楚,再来看下面这段代码:

1
2
3
4
5
6
7
List<? extends Number> l1 = new ArrayList<Number>();
List<? extends Number> l2 = new ArrayList<Integer>();
List<? extends Number> l3 = new ArrayList<Double>();

l1.add(2); // 编译不通过
l2.add(2); // 编译不通过
l3.add(3); // 编译不通过

在上面的代码中,我们声明了 3 个 list,这没什么问题,但是当我们分别向 3 个 list 中添加元素的时候,居然会报错。因为 <? extends Number> 只能保证这个 List 中的内容都是 Number 类型的,当你添加一个 Integer 时,list 实际有可能指向 List<Double>,所以编译器不允许向 list 中添加元素,也就是说 <? extends T> 类型的 List 是只读的。

同样从 list 中获取值时,它也只能保证是 Number 类型的,所以无法从中获取 Integer 或者 Double 等类型的数据(可以取出来之后进行强制类型转换)。

也就是说 <? extends T> 中只能放 T 类型或其子类的对象。

<? super T>

搞懂了 extends ,那么 super 这个就不难懂了,看下面的代码:

1
2
3
4
5
List<? super Number> l1 = new ArrayList<Number>();
List<? super Number> l2 = new ArrayList<Object>();

l1.add(1); // 编译通过
l2.add(2.2); // 编译通过

这里的 list 可以添加元素,只能添加 Number 或者 Number 的子类对象。但是不能向 list 中添加 Object 的类型,因为这个 list 的实际类型有可能是 List<Number>

从 list 取值的时候,只能取出 Object 类型的值,因为只能保证取出来的值是 Object 的子类对象。

<? super T> 和 <? extends T> 可以组合起来使用(Collections.copy(),删减了一些代码),如下例子:

1
2
3
4
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));
}

PECS

PECS 的全称是:Producer extends and Consumer super。

可以认为 PECS 说明了这两个通配符的使用场景。

这里以 List 为例:

  • Producer extends:如果你需要一个 List 来为你生成 T 类型的数据(也就从 list 中读取 T 类型的数据),那就把这个 List 声明为 <? extends T>,比如 List<? extends Integer>。但是无法向 list 中添加元素。
  • Consumer super:如果你需要一个 List 来消费 T 类型的数据(也就是将 T 类型的数据写入到 list中),那就把这个 List 声明为 <? super T>,如 List<? super Integer>,但是只能从中取出 Object 类型的数据(需要自己进行强制转换)。
  • 如果你需要对一个 List 同时进行读和写,那就不要使用通配符。如:List<Integer>

泛型擦除问题

<? extends T> 和 <? super T> 除了上面的用法之外,还能部分解决泛型擦除所带来的问题。

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,所以最后泛型数据其实就是被保存成了 Object,这样会带来一些问题,看下面的代码:

1
2
3
4
5
6
7
8
9
List<String> strList = new ArrayList<String>();

try {
strList.getClass().getMethod("add", Object.class).invoke(strList, 1);
} catch (Exception e) {
e.printStackTrace();
}

System.out.println("strList size: "+ strList.size()); // strList size: 1

是的,没错,利用反射机制把 Integer 类型的数据插入到了 List<String> 中,这肯定是有问题的,在获取数据时就会发生强制类型转换错误

这当然是一个比较极端的情况,而且 Java 目前也没有办法完全把这个问题解决。但是使用 <? extends T> 让避免对象被擦除成 Object,如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Demo <T extends String> {     
public void method(T param) {
System.out.println(param);
}
}

Class clazz = Demo.class;
Field[] fs = clazz.getDeclaredFields();
for ( Field f:fs) {
System.out.println("Field name: "+f.getName()+", field type:"+f.getType().getName());
// Field name: data, field type:java.lang.String
}

微信公众号

© 2020 ray