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 1
class Food { }
// lev 2
class Fruit extends Food { }
class Meat extends Food { }
// lev 3
class Apple extends Fruit { }
class Banana extends Fruit { }
class Pork extends Fruit { }
class Beef extends Fruit { }
// lev 4
class 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() 方法, 插入Apple
和Fruit
都报错.
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
//不能存入任何元素
p.set(new Fruit()); //Error
p.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(); //Error
Fruit newFruit1=p.get(); //Error
Object 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子类型限定的通配符可以向泛型对象「读取」