vlambda博客
学习文章列表

Java基础: 泛型 <? super T> 中 super与extends的理解


2022年文章分类汇总
http://www.wt1814.com/view/docs/a-share/wechat/2022share.html
2021年文章分类汇总
http://www.wt1814.com/view/docs/a-share/wechat/2021share.html
2020年文章分类汇总
http://www.wt1814.com/view/docs/a-share/wechat/2020share.html



文章结合知乎大佬的回答,结合自己的理解进行整理.但内容属于该作者. 原文链接: 点击跳转

<? extends T><? super T> 是Java泛型中的「通配符」和「边界」的概念.

<? extends T> : 是指 上界通配符
<? super T> : 是指 下届通配符

1. 为什么要用通配符和边界?

使用泛型的过程中,经常出现一种很别扭的情况.比如我们有Fruit类,和它的派生类Apple类.

class Fruit { } 
class Apple extends Fruit { }

然后有一个最简单的容器: 「Plate」类,盘子里可以放一个泛型的"「东西」".我们可以对这个东西做最简单的"放"和"取"的动作: set() 和 get() 方法.

class Plate<T> {
private T item;
public Plate(T t) { item = t; }
public void set(T t) { item = t; }
public T get() { return item; }}
现定义一个"水果盘子",逻辑上水果盘子当然可以装苹果.
Plate<Fruit> p = new Plate<>(new Apple());

但实际上Java编译器不允许这个操作.会报错,“装苹果的盘子"无法转化为"装水果的盘子”.

这个不符合正常的逻辑呀.

但编译器认定的逻辑是这样的:

苹果 IS-A 水果
装苹果的盘子 NOT-IS-A 装水果的盘子

所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的.

所以我们不可以把 Plate<Apple>的引用传递给 Plate<Fruit>.

为了让泛型用起来更舒服,Sun的大脑袋们就想出了 <? extends T><? super T> 的办法, 来让"水果盘子"和"苹果盘子"之间发生关系.

2. 什么是通配符?

在使用泛型类的时候,既可以指定一个具体的类型,如List就声明了具体的类型是String;

也可以用通配符? 来表示未知类型,如List<?> 就声明了List中包含的元素是未知的.

通配符所代表的其实是一组类型, 但具体的类型是未知的. List<?>所声明的就是所有类型都是可以的.但是List<?>并不等同于List

List实际上确定了List中包含的是Object及其子类,在使用的时候就可以通过Object来进行引用.而List<?> 其中所包含的元素类型是不确定. 其中可能包含的是String,也可能是Integer. 如果它包含了String的话,往里面添加Integer类型的元素就是错误的. 正因为类型未知, 就不能通过new ArrayList<?>() 方法来创建一个新的ArrayList 对象. 因为编译器无法知道具体的类型是什么,但是对于List<?>中的元素却总是可以用Object 来引用了,因为虽然类型未知, 但肯定是Object及其子类.

考虑下面的代码:

public void wildcard(List<?> list) {
list.add(1);// 编译错误
}

如上所示,试图对一个带通配符的泛型类进行操作的时候, 总是会出现编译错误.其原因在于通配符所表示的类型是未知的.

这就是三句话总结Java泛型通配符(PECS)中的第一句话: ?不能添加元素,只能作为消费者.

因为对于List<?> 中的元素只能用Object 来引用,在有些情况下不是很方便. 在这些情况下, 可以使用上下界来限制未知类型的范围. 如List<? extends Number> 说明List中可能包含的元素类型是Number及其子类,而List<? super Number>则说明List中包含的是Number及其父类.当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法.比如访问List<? extends Number>的时候, 就可以使用Number类的intValue等方法.

3. 什么是上界?

下面代码就是"上界通配符":

Plate <? extends Fruit>

翻译成人话就是: 一个能放水果以及一切是水果派生类的盘子.

再直白一点就是: 啥水果都能放的盘子. 这和我们人类的逻辑就比较接近了.

Plate <? extends Fruit>Plate <Apple> 最大的区别就是 Plate <? extends Fruit>Plate <Apple> 的基类.

直接的好处就是: 我们可以用"苹果盘子"给水果盘子"赋值了.

Plate<? extends Fruit> p = new Plate<>(new Apple());

如果把Fruit和Apple 的例子再拓展一下, 食物分成水果和肉类, 水果有苹果和香蕉, 肉类有猪肉和牛肉,苹果还有两种 青苹果和红苹果.

// lev 1class Food { }
// lev 2class Fruit extends Food { }class Meat extends Food { }
// lev 3class Apple extends Fruit { }class Banana extends Fruit { }class Pork extends Fruit { }class Beef extends Fruit { }
// lev 4class RedApple extends Apple { }class GreenApple extends Apple {  }

在这个体系中,上界通配符 "Plate <? extends Fruit> " 覆盖下图中蓝色的区域.

4. 什么是下界?

相对应的, “下界通配符”

Plate <? super Fruit> 

表达的就是相反的概念: 一个能放水果以及一切是水果基类的盘子.

Plate <? super Fruit> 是Plate 的基类,但不是Plate 的基类. 对应刚刚那个例子, Plate <? super Fruit> 覆盖下图中红色的区域.

5. 上下界通配符的副作用

边界让Java不同泛型之间的转换更容易了.但不要忘记,这样的转换也有一定的副作用.那就是「容器的部分功能可能失效」.

还是以刚才的Plate为例, 我们可以对盘子做两件事, 往盘子里set() 新东西,以及从盘子里 get ()东西.

class Plate<T>{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}

5.1 上界<? extends Fruit> 不能往里存, 只能往外取.

(1) <? extends Fruit> 会使往盘子里放东西的set() 方法失效, 但取东西get() 方法还有效
(2) 取出来的东西只能存放在Fruit 或它的基类里面,向上造型.

比如下面例子里两个set() 方法, 插入AppleFruit都报错.

Plate<? extends Fruit> p=new Plate<Apple>(new Apple()); //不能存入任何元素p.set(new Fruit()); //Errorp.set(new Apple()); //Error //读取出来的东西只能存放在Fruit或它的基类里。Fruit newFruit1=p.get();Object newFruit2=p.get();Apple newFruit3=p.get();    //Error


编译器只知道容器内是Fruit 或者它的派生类, 但具体是什么类型不知道,因此取出来的时候要向上造型为基类.

可能是Fruit? 可能是Apple? 也可能是Banana, RedApple, GreenApple? 编译器在看到后面用Plate赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:capture#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号capture#1。

然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个capture#1匹配,所以就都不允许。

所以通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。

比如下面这个泛型方法里, 三个T都指代同一个类型,要么都是String, 要么都是Integer…

public <T> List<T> fill(T... t);

但通配符 <?> 没有这种约束, Plate<?> 单纯的就表示: 「盘子里放了一个东西,是什么我不知道。」

5.2 下界<? super T>不影响往里存,但往外取只能放在Object对象里

(1) 使用下界<? super Fruit> 会使从盘子里取东西的get( ) 方法部分失效, 只能存放在Object对象里.

因为规定的下界,对于上界并不清楚,所以只能放到最根本的基类Object中.

(2) set( ) 方法正常.

Plate<? super Fruit> p=new Plate<Fruit>(new Fruit()); //存入元素正常p.set(new Fruit());p.set(new Apple()); //读取出来的东西只能存放在Object类里。Apple newFruit3=p.get(); //ErrorFruit newFruit1=p.get(); //ErrorObject newFruit2=p.get();

因为下界规定了元素的最小粒度的下限, 实际上是放松了容器元素的类型控制.

既然元素是Fruit的基类, 那往里存粒度比Fruit 小的都可以.

但往外读取元素就费劲了, 只有所有类的基类Object 对象才能装下.但这样的话, 元素的类型信息就全部丢失.

6. PECS原则

最后看一下什么是PECS(Producer Extends Consumer Super)原则,已经很好理解了.

  • Producer Extends 生产者使用Extends来确定上界,往里面放东西来生产

  • Consumer Super 消费者使用Super来确定下界,往外取东西来消费

(1). 频繁往外读取内容的,适合用上界Extends ,即extends可用于的返回类型限定,不能用于参数类型限定.
(2). 经常往里插入的,适合用下界Super,super可用于参数类型限定,不能用于返回类型限定.
(3). 带有super超类型限定的通配符可以向泛型对象用「写入」,带有extends子类型限定的通配符可以向泛型对象「读取」