搞明白 Java 的通配符泛型

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

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

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

新建和表示一个泛型类变量
List list = new ArrayList<>();
泛型类的定义中
public interface List
函数定义中
T[] toArray(T[] a)
那么,一般来说,我们的通配符泛型只适用于:

函数中的参数部分

比如 Collections.copy() 方法

public static void copy(List<? super T> dest,List<? extends T> src)
或者是 Stream.map() 方法

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

List<? extends Number> list = new ArrayList<>(); // 这个代码没有任何用处!
没有用处的原因可以接着往下看。

为什么要用通配符泛型
我们现在有这样一个函数

public void test(List data) {

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

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

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

只能选择一个方向,要么选 【List 和 List】 要么选 【List 和 List】
有副作用
通配符泛型的方向和限制
我们先看一下 List 的接口

public interface List { // 固定一个类型 E

E get(int index);
boolean add(E e);

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

向父类扩大泛型 <? super T>
super 在这里也叫父类型通配符

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

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

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

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

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

Object object = data.get(1); // 只能用 Object 接住变量

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

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

Integer i = 5;
data.add(i);

}
向子类扩大泛型 <? extends T> 和 <?>
extends 在这里也叫子类型通配符

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

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

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

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

Number number = data.get(2);

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

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

Integer i = 5;
data.add(i); // 错误,无法通过编译

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

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

ArrayList.forEach
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 ,我们可以传一个 Consumer 也可以传一个 Consumer,表示的意思是,既然你可以消费 XXX 的父类,那么,我也可以把你的子类传给你。

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

public interface Consumer {

void accept(T t);

}
Consumer numberConsumer = number -> System.out.println(number.doubleValue());
Collections.copy
public static 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

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

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

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

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

public static void copy(List 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
从这里开始,就讲的比较复杂了:

public interface Stream {

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

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

Stream stringStream =

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

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

我们先看一下 Function 接口

public interface Function<T, R> {

R apply(T t);

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

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

public Stream apply(Integer integer) {

return Stream.of("1", "2");

}
回到我们的 flatMap 函数,这里的 T 已经在 Stream 创建的时候确定了,我们以 Stream 为例,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 的类型,从而改变返回值类型。

Stream numberStream =

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

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

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

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

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

关于为什么在普通代码中

List<? extends Number> list = new ArrayList<>();
没有用的原因,因为你创建了之后,因为 extends 的副作用,你根本没法修改这个 ArrayList 。 所以在普通代码中,用到 通配符泛型的情景很少。

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

原创文章:https://www.qqhhs.com,作者:起航®,如若转载,请注明出处:https://www.qqhhs.com/65.html

版权声明:本站提供的一切软件、教程和内容信息仅限用于学习和研究目的,请于下载的24小时内删除;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络收集整理,如果您喜欢该程序和内容,请支持正版,购买注册,得到更好的正版服务。我们非常重视版权问题,如有侵权请邮件与我们联系处理。敬请谅解!

Like (0)
Donate 受君之赠,荣幸之至 受君之赠,荣幸之至 受君之赠,荣幸之至 受君之赠,荣幸之至
Previous 2023年1月10日
Next 2023年1月10日

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

qhhl

QQ-175142992024110802215838同号

SHARE
TOP
“人们不愿意相信,一个土匪的名字叫牧之”