@version:1.1-RELEASE

本文目的解决:已知道有泛型,泛型统配符有什么用?是用来干嘛的?要怎么用?

为什么要存在泛型通配符

首先准备下面类图中的几个类便于理解。

POJO.png

  1. 在我们生活的真实世界中,如果一个盘子可以装水果,那么它就可以是一个装苹果的盘子,也可以是一个装橙子的盘子。
  2. 我们又已知Java三大特性之一的:多态。

通过上面两点,结合实际生活和代码知识,可以推导出以下表达式:

  1. Plate<Fruit> plate = new Plate<Apple>();

但是可惜,编译无法通过。
Snipaste_2020-03-05_22-22-58.png

编译器认为“苹果盘子 is a 水果盘子”这个表达式不成立。 苹果盘子 is not a 水果盘子。

但是下面这段代码却是成立的:

  1. Fruit fruit = new Apple();

也就是说苹果和水果之间有继承关系是is a的关系,但苹果盘子和水果盘子之间是没有继承关系的。

泛型通配符用来干嘛的

假设我有很多种的水果盘子,如果我想用一种统一的方式来“使用”这些盘子该怎么做呢?上面的例子已经说明水果盘子和苹果盘子之间无法赋值引用,Sun的大神们也同样碰到过这个问题,于是大神们引入了泛型统配符来达到这个目的。

下来来看这段代码:

  1. Plate<? super Apple> applePlate1;
  2. Plate<? extends Apple> applePlate2;
  3. Plate<Food> foodPlate = new Plate<>();
  4. Plate<Fruit> fruitPlate = new Plate<>();
  5. Plate<Apple> applePlate = new Plate<>();
  6. Plate<Orange> orangePlate = new Plate<>();
  7. Plate<RedApple> redApplePlate = new Plate<>();
  8. applePlate1 = foodPlate; // 成立
  9. applePlate1 = fruitPlate; // 成立
  10. applePlate1 = applePlate; // 成立
  11. applePlate1 = orangePlate; // 错误
  12. applePlate1 = redApplePlate; // 错误
  13. applePlate2 = foodPlate; // 错误
  14. applePlate2 = fruitPlate; // 错误
  15. applePlate2 = applePlate; // 成立
  16. applePlate2 = orangePlate; // 错误
  17. applePlate2 = redApplePlate; // 成立

实践是检验真理的唯一标准。

通过上面的代码我们已经可以知道结果了,通过结果反推结论。

Plate<? super Apple>:它是苹果盘子,也是水果盘子,也是食物盘子,是一种苹果的基类的盘子。
Plate<? extends Apple>:它是苹果盘子,也是红苹果盘子,是一种苹果的派生类的盘子。

<? super T> 表示是T或者T的基类

<? extends T> 表示是T或者T的派生类

下界和上界是怎么一回事

Snipaste_2020-03-05_22-49-11.png

回到类图上,可以看到<? super Apple> 和 <? extends Apple> 在 Apple 所在的分支上,以 Apple 类为界线将这个类图分为两部分,有这么一个特点:以 Apple 作为下边界的是 Apple 的基类;以 Apple 作为上边界的是 Apple 的派生类。
在这里我们不提 Orange,因为两者都嫌弃它。

根据这个特点,我们将<? super T>叫做下界,将<? extends T> 叫做上界。

借用网络上的一张图,可以更形象的表达下界、上界的含义。
5265216-7492a3b031ec1f60.webp
5265216-260e81d8c494ab84.webp

上下边界带来的副作用

<? super T> 和 <? extends T> 除了所表示能接收的类范围不相同之外还有其他差别吗?

THOW ME THEN CODE!

  1. Plate<? super Apple> applePlate1;
  2. Plate<? extends Apple> applePlate2;
  3. applePlate1.setItem(new Food()); // 错误
  4. applePlate1.setItem(new Fruit()); // 错误
  5. applePlate1.setItem(new Apple()); // 成立
  6. applePlate1.setItem(new RedApple()); // 成立
  7. applePlate1.setItem(new Orange()); // 错误
  8. applePlate2.setItem(new Food()); // 错误
  9. applePlate2.setItem(new Fruit()); // 错误
  10. applePlate2.setItem(new Apple()); // 错误
  11. applePlate2.setItem(new RedApple()); // 错误
  12. applePlate2.setItem(new Orange()); // 错误

通过上面的代码可以发现:

  • 对于下界,即被 <? super Apple> 修饰的:只要是边界(Apple)及以下的都可以放进去,边界以上的不行。
  • 对于上界,即被 <? extends Apple> 修饰的:无论是边界的哪边都无法放进去。

由此可以总结出:

对于下界 <? super T> 可以放入T或T的派生类。 对于上界 <? extends T> 什么都不能放入。

继续!

  1. Plate<? super Apple> applePlate1;
  2. Plate<? extends Apple> applePlate2;
  3. Food food1 = applePlate1.getItem(); // 错误
  4. Fruit fruit1 = applePlate1.getItem(); // 错误
  5. Apple apple1 = applePlate1.getItem(); // 错误
  6. RedApple redApple1 = applePlate1.getItem(); // 错误
  7. Orange orange1 = applePlate1.getItem(); // 错误
  8. Food food2 = applePlate2.getItem(); // 成立
  9. Fruit fruit2 = applePlate2.getItem(); // 成立
  10. Apple apple2 = applePlate2.getItem(); // 成立
  11. RedApple redApple2 = applePlate2.getItem(); // 错误
  12. Orange orange2 = applePlate2.getItem(); // 错误

通过上面的代码可以发现:

  • 对于上界,即被 <? extends Apple> 修饰的:只要是边界(Apple)及以上的都可以取出来。
  • 对于下界,即被 <? super Apple> 修饰的:无论是边界的哪边都无法取出来。

由此可以总结出:

对于下界 <? super T> 不可以取。 对于上界 <? extends T> T或T的基类可以取出。

现象解释

对于下界 <? super T> 可以存T或T的派生类,但不能取; 对于上界 <? extends T> 不能存,但是可以从里面取出T或T的基类;

怎么解释这种现象呢?

Main.png

在 Java 中有种现象叫“向上转型”,更细粒度的类可以由其基类直接接收,编译器会自动完成转型的过程。 Java 是单继承的,所有的继承类构成一个棵树,粒度粗的在上面,粒度细的在下面;相邻子树无法完成转型。

<? super T> 代表类的类型是 T 的基类,但不确定是 Class3 还是 Class2 或者 Class1(如上图),只能确定肯定是 T 的基类,所以只要是 T 的子类都可以放进去,因为编译器帮你完成了向上转型;但是不能取出来,因为无法确定 T 的基类到底是哪个(如果你用了 Class2 来接收实际的 Class1 ,向下转型无法由编译器自动完成),但因为所有的类都继承于 Object,所以还是可以使用 Object 来接收,但如果使用 Object 就失去泛型的意义了所以本帖不做说明。

<? extends T> 代表类的类型是 T 的派生类,T 的派生类有好多无法确定是哪个编译器无法完成自动转型(比如如果你想把 Class6 放进去,但实际类型是 Class5)所以无法放入;但可以确定的是里面的类型肯定是 T 的派生类所以可以使用 T 以及 T 的基类来接收。

泛型通配符的应用

上界不能往里面放,下界不能往外取。不放怎么取?不取放了有什么用?

根据上界、下界的特点,引出来一个原则:PECS

如果你想从一个数据类型里获取数据(生产),使用 <? extends T> 通配符 如果你想把对象写入一个数据结构里(消费),使用 <? super T> 通配符 如果你既想存,又想取,那就别用通配符。

如:java.util.function.Function#andThen,入参类型是下界,返回值类型是上界;使用下界是使得对入参要求更严格,使用上界作为返回值类型放宽要求,通过两者共同保证方法的正确性、健壮性。

  1. default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
  2. Objects.requireNonNull(after);
  3. return (T t) -> after.apply(apply(t));
  4. }

特别说明

行尾注释是不规范的,在这个使用行尾注释只是因为语句没有复杂的含义,放在行尾能更好的表达正确还是错误。

参考资料

困扰多年的Java泛型 extends T> super T>,终于搞清楚了!
java 泛型中的上界(extend)和下界(super)
Java泛型之上、下边界通配符的理解(适合初学)
PECS法则与extends和super关键字
怎样理解java中Function<? super V, ? extends T>这样的声明?