写在前面

Java 泛型中的通配符泛型问题困扰我很久,即 <? super T> 和 <? extends T> 和 <?> 这几种泛型,到底是什么,到底怎么用。从含义上理解, super 是指 T 和 T 的父类,extends 是指 T 和 T 的子类。网上有一个简单的原则叫PECS(Producer Extends Consumer Super)原则:往外读取内容的,适合用上界 Extends,往里插入的,适合用下界 Super。不过这个原则讲得很含糊,而且没有考虑到全部情境,所以我写一篇文章再来讲一下这几个泛型到底怎么用。

通配符泛型用在哪里?

网上很多资料连用在哪里都没有说清楚,导致我们用的时候一头雾水,在这里我有必要先说清楚。

首先,我们先说泛型 ,会在三个地方用到(不是通配符泛型):

  • 新建和表示一个泛型类变量
1
List<String> list = new ArrayList<>();
  • 泛型类的定义中
1
public interface List<E>
  • 函数定义中
1
<T> T[] toArray(T[] a)

那么,一般来说,我们的通配符泛型只适用于:

函数中的参数部分

比如 Collections.copy() 方法

1
public static <T> void copy(List<? super T> dest,List<? extends T> src)

或者是 Stream.map() 方法

1
<R> Stream<R> map(Function<? super T, ? extends R> mapper)

从语法上说,用在新建和表示一个泛型类变量也可以用,但是如果不在通配符泛型作参数的函数中使用,没有任何用处,请不要被网上的资料的 demo 误导。

1
List<? extends Number> list = new ArrayList<>(); // 这个代码没有任何用处!

没有用处的原因可以接着往下看。

为什么要用通配符泛型

我们现在有这样一个函数

1
2
3
public void test(List<Number> data) {

}

根据泛型规则,这个函数只能传进来 List<Number> 一种类型,我想传 List<Object>List<Integer> 都是传不进去的。

但是,我既要泛型,又想把这两个类型的子类或者父类的泛型传进去,可不可以呢,是可以的,就是使用通配符泛型。

但是,通配符泛型限制也很多:

  1. 只能选择一个方向,要么选 【List 和 List】 要么选 【List 和 List

  2. 有副作用

通配符泛型的方向和限制

我们先看一下 List 的接口

1
2
3
4
public interface List<E> { // 固定一个类型 E
E get(int index);
boolean add(E e);
}

get() 方法的返回值和 E 关联,我们姑且称之为取返回值
而 add() 方法是参数和 E 关联,我们姑且称之为传参数

向父类扩大泛型 <? super T>

super 在这里也叫父类型通配符

我们把上面的函数升级一下,变成下面的方法

1
2
3
public void test(List<? super Number> data) {

}

那么,现在,好消息是,我既可以传 List<Number> ,也可以传 List<Object> 进上面的函数。

但是,从 向父类扩大泛型的 List 的获取返回值【E get(int i)】的时候, E 的类型没有办法获取了,因为不知道你传进去的到底是 List<Number> 还是 List<Object>,所以统一向上转 E 为 Object

1
2
3
public void test(List<? super Number> data) {
Object object = data.get(1); // 只能用 Object 接住变量
}

而往 向父类扩大泛型的 List 传参数【add(E e) 】时,只要是 Number 或者 Number 子类,都可以传。因为不管你 E 是 Number 还是 Object ,我传一个 Integer 进去总是可以的。

1
2
3
4
public void test(List<? super Number> data) {
Integer i = 5;
data.add(i);
}

向子类扩大泛型 <? extends T> 和 <?>

extends 在这里也叫子类型通配符

我们把上面的函数升级一下,变成下面的方法

1
2
3
public void test(List<? extends Number> data) {

}

那么,现在,好消息是,我既可以传 List<Number> ,也可以传 List<Integer> 进去
但是,从 向子类扩大泛型的 List 的获取返回值【E get(int i)】的时候,E 的类型被统一为 Number,因为不知道你传进去的到底是 List<Number> 还是List<Integer>,返回的时候都可以向上转到 Number。

1
2
3
public void test(List<? extends Number> data) {
Number number = data.get(2);
}

而往 向子类扩大泛型的 List 传参数【add(E e) 】时,你不可以传。因为 E 这个时候没法确定了。因为你有可能传 List<Number> List<Integer> List<Double>,而 e 如果是一个 Number,是传不进子类的参数类型的,比如现在传进来一个 List<Integer>,那函数就变成 add(Integer e),你不能传一个 Number 进来,所以不可以往这个 向子类扩大泛型的 List 传参数

1
2
3
4
public void test(List<? extends Number> data) {
Integer i = 5;
data.add(i); // 错误,无法通过编译
}

还有一个 <?> 有什么用呢?它等价于 <? extends Object> ,具体用的时候和没有泛型大体一致。

怎么用?JDK 中的使用例子

相信你看完上面的限制之后,已经不再想用这个麻烦的玩意了,或者更加奇怪为什么要设计一个这样的东西出来。让我们看一下 JDK 里面的用法吧。

ArrayList.forEach

1
2
3
4
5
6
7
8
9
10
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
final Object[] es = elementData;
final int size = this.size;
for (int i = 0; modCount == expectedModCount && i < size; i++)
action.accept(elementAt(es, i));
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

表示消费 E 或者 E 的父类的消费者可以消费这些元素。

比如对于一个 ArrayList<Integer> ,我们可以传一个 Consumer<Integer> 也可以传一个 Consumer<Number>,表示的意思是,既然你可以消费 XXX 的父类,那么,我也可以把你的子类传给你。

<? super E> 的向父类扩大泛型,向 action 取返回值有影响,向 action 传参数没有影响。而 Consumer本身就是一个没有返回值的接口。

1
2
3
public interface Consumer<T> {
void accept(T t);
}
1
Consumer<Number> numberConsumer = number -> System.out.println(number.doubleValue());

Collections.copy

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());
}
}
}

这个函数可以把一个 Integer 的 List 转成 Number 的 List

1
Collections.copy(new ArrayList<Number>(),new ArrayList<Integer>());

这里不知道你有没有疑问,为什么它既用 super 又用 extends 呢,因为这里用于静态函数,所以T的类型是调用时才确定,那么T到底应该是 Integer 还是 Number 呢,虽然这不影响最终调用结果,但这多少给调用者造成一些困惑。

还有第二个问题,按照我们上面说的,用了 super 之后,取返回值的话,会有一个限制,即强转到 Object。

有人认为这是该函数的作者强调 PECS 原则,但是在这个情境下,这个原则并不合适。

其实,我们可以只把 T 固定为 Number,然后少用一个 <? super T> ,既可以解决歧义,同时又避免函数内部取返回值时强转到 Object 。

1
public static <T> void copy(List<T> dest, List<? extends T> src)

参考:https://stackoverflow.com/questions/34985220/differences-between-copylist-super-t-dest-list-extends-t-src-and-co

我再提一下很流行的 PECS 原则:往外读取内容的,适合用上界 extends,往里插入的,适合用下界 super。这句话确实没错,用来解释这个函数,dest是被写入的,用 super ,src 是读取的,用 extends

然而,PECS 还漏了一种情况,就是我不用上下界的时候,我既可以读,也可以插入。如果条件允许,比如这个函数中的 是根据参数类型确定的,我们应该优先使用 T,而不是生搬硬套 PECS 原则。

Stream.flatMap

从这里开始,就讲的比较复杂了:

1
2
3
public interface Stream<T> {
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
}

flatMap 是 一个什么函数呢?他可以把 Stream 里面的元素转成 Stream,再在内部合并,比如 Stream<Integer>,对于每一个 Integer 都执行一次这个 mapper, Integer 经过这个 mapper ,就变成 Stream,最后再把所有 Stream 合并成一个 Stream,再返回。

1
2
Stream<String> stringStream = 
new ArrayList<Integer>().stream().flatMap(integer -> Stream.of("1", "2"));

使用示例,一个 Integer 的流转成了一个 String 的流,如果原来是 [1,1] ,那么现在变成 [“1”,”2”,”1”,”2”]

我们先看一下 Function 接口

1
2
3
public interface Function<T, R> {
R apply(T t);
}

意思就是输入一个 T 类型的参数,返回一个 R 类型的返回值

我们的 integer -> Stream.of(“1”, “2”) 也可以写成这样

1
2
3
public Stream<String> apply(Integer integer) {
return Stream.of("1", "2");
}

回到我们的 flatMap 函数,这里的 T 已经在 Stream 创建的时候确定了,我们以 Stream<Integer> 为例,T就是 Integer
我们看到 Function中的 T 类型是: ? super T 意味着不光 Integer 可以作为 Function 的传入参数,它的父类也可以,比如 Number,上面例子是 Integer
接着是定义 R 的类型即返回值类型:? extends Stream<? extends R>,对应例子里面是 Stream 的识别
先看? extends Stream,为什么要有这个呢,因为 Stream 是接口,而有时候我们可能会传一个 Stream 的实现类进去(当然,这个机会很少),这样就放宽了门槛。上面的例子返回的 Stream 是 Stream
接着看? extends R,这里的 R 包括 R 和 R 的子类,R由输入的 Function 的泛型 Stream 确定,这个例子里面是 String。那么既然总是可以通过输入的参数确定R,那 extends R 有什么用呢?这样写可以多一个功能,这样你可以显式修改 R 的类型,从而改变返回值类型。

1
2
Stream<Number> numberStream = 
new ArrayList<Integer>().stream().<Number>flatMap(integer -> Stream.of(1, 2));

原来应该返回 Stream<Integer> ,但是现在被我在 flatMap 前面用 显式指定了 R 的类型,这样子 最后返回 Stream 的时候不再是 Stream<Integer>

而反观 Colletions.copy 也有类似的 <? super T> ,因为 T 总是可以被输入的参数确定,而和上面的不同的是,这个即使显式指定,也无法修改返回值,所以除了副作用没别的作用,所以我还是坚持我的看法。

总结

虽然说上面的例子看起来比较难懂,但是说实话,在我们平常的开发中,通配符泛型并没有经常用到,我们只需要调用库的时候看懂库的参数是什么意思就好。

我简单的再分析下两个通配符泛型的使用场景:
<? super T> 可能会在一些消费者的函数里面用到,比如参数是 Consumer 接口的时候,我们可以带上一个 super T
<? extends T> 的副作用是比较大的,适用于给多种不同的子类的集合做归约操作,比如有 List<Integer> List<Double>,你可以写一个函数统一处理 List <? extends Number> 。
另外,在写完一个带泛型参数的函数之后,我们可以思考一下要不要用通配符泛型扩大范围,从而让我们的函数更加具有通用性。

关于为什么在普通代码中

1
List<? extends Number> list = new ArrayList<>();

没有用的原因,因为你创建了之后,因为 extends 的副作用,你根本没法修改这个 ArrayList 。 所以在普通代码中,用到 通配符泛型的情景很少。

关于 PECS,我至今没记住这几个英文单词的顺序,我认为不能生搬硬套,还是要根据实际情况分析是否合理。因为 PECS 最大的问题是它只告诉你用通配符泛型的情景下你应该如何选择,没有告诉你什么时候用 通配符泛型,什么时候不用。