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 |
publicinterfaceList<E> |
- 函数定义中
1 |
<T> T[] toArray(T[] a) |
那么,一般来说,我们的通配符泛型只适用于:
函数中的参数部分
比如 Collections.copy() 方法
1 |
publicstatic <T> voidcopy(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 |
publicvoidtest(List<Number> data){ } |
根据泛型规则,这个函数只能传进来 List<Number>
一种类型,我想传 List<Object>
和 List<Integer>
都是传不进去的。
但是,我既要泛型,又想把这两个类型的子类或者父类的泛型传进去,可不可以呢,是可以的,就是使用通配符泛型。
但是,通配符泛型限制也很多:
- 只能选择一个方向,要么选 【List 和 List】 要么选 【List 和 List】
- 有副作用
通配符泛型的方向和限制
我们先看一下 List 的接口
1 2 3 4 |
publicinterfaceList<E> { // 固定一个类型 E E get(int index); booleanadd(E e); } |
get() 方法的返回值和 E 关联,我们姑且称之为取返回值。
而 add() 方法是参数和 E 关联,我们姑且称之为传参数。
向父类扩大泛型 <? super T>
super 在这里也叫父类型通配符
我们把上面的函数升级一下,变成下面的方法
1 2 3 |
publicvoidtest(List<? super Number> data){ } |
那么,现在,好消息是,我既可以传 List<Number>
,也可以传 List<Object>
进上面的函数。
但是,从 向父类扩大泛型的 List 的获取返回值【E get(int i)】的时候, E 的类型没有办法获取了,因为不知道你传进去的到底是 List<Number>
还是 List<Object>
,所以统一向上转 E 为 Object
1 2 3 |
publicvoidtest(List<? super Number> data){ Object object = data.get(1); // 只能用 Object 接住变量 } |
而往 向父类扩大泛型的 List 传参数【add(E e) 】时,只要是 Number 或者 Number 子类,都可以传。因为不管你 E 是 Number 还是 Object ,我传一个 Integer 进去总是可以的。
1 2 3 4 |
publicvoidtest(List<? super Number> data){ Integer i = 5; data.add(i); } |
向子类扩大泛型 <? extends T> 和 <?>
extends 在这里也叫子类型通配符
我们把上面的函数升级一下,变成下面的方法
1 2 3 |
publicvoidtest(List<? extends Number> data){ } |
那么,现在,好消息是,我既可以传 List<Number>
,也可以传 List<Integer>
进去
但是,从 向子类扩大泛型的 List 的获取返回值【E get(int i)】的时候,E 的类型被统一为 Number,因为不知道你传进去的到底是 List<Number>
还是List<Integer>
,返回的时候都可以向上转到 Number。
1 2 3 |
publicvoidtest(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 |
publicvoidtest(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 |
publicvoidforEach(Consumer<? super E> action){ Objects.requireNonNull(action); finalint expectedModCount = modCount; final Object[] es = elementData; finalint size = this.size; for (int i = 0; modCount == expectedModCount && i < size; i++) action.accept(elementAt(es, i)); if (modCount != expectedModCount) thrownew ConcurrentModificationException(); } |
表示消费 E 或者 E 的父类的消费者可以消费这些元素。
比如对于一个 ArrayList<Integer>
,我们可以传一个 Consumer<Integer>
也可以传一个 Consumer<Number>
,表示的意思是,既然你可以消费 XXX 的父类,那么,我也可以把你的子类传给你。
<? super E> 的向父类扩大泛型,向 action 取返回值有影响,向 action 传参数没有影响。而 Consumer本身就是一个没有返回值的接口。
1 2 3 |
publicinterfaceConsumer<T> { voidaccept(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 |
publicstatic <T> voidcopy(List<? super T> dest, List<? extends T> src){ int srcSize = src.size(); if (srcSize > dest.size()) thrownew 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 |
publicstatic <T> voidcopy(List<T> dest, List<? extends T> src) |
我再提一下很流行的 PECS 原则:往外读取内容的,适合用上界 extends,往里插入的,适合用下界 super。这句话确实没错,用来解释这个函数,dest是被写入的,用 super ,src 是读取的,用 extends
然而,PECS 还漏了一种情况,就是我不用上下界的时候,我既可以读,也可以插入。如果条件允许,比如这个函数中的 是根据参数类型确定的,我们应该优先使用 T,而不是生搬硬套 PECS 原则。
Stream.flatMap
从这里开始,就讲的比较复杂了:
1 2 3 |
publicinterfaceStream<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 |
publicinterfaceFunction<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 最大的问题是它只告诉你用通配符泛型的情景下你应该如何选择,没有告诉你什么时候用 通配符泛型,什么时候不用。