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

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

是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的引用传递给 Plate.

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

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 说明List中可能包含的元素类型是Number及其子类,而List则说明List中包含的是Number及其父类.当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法.比如访问List的时候, 就可以使用Number类的intValue等方法.

3. 什么是上界?

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

Plate <? extends Fruit>

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

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

Plate Plate 最大的区别就是 Plate Plate 的基类.

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

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 " 覆盖下图中蓝色的区域.

Java基础: 泛型 <? super T> 中 super与extends的理解_第1张图片

4. 什么是下界?

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

Plate <? super Fruit> 

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

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

Java基础: 泛型 <? super T> 中 super与extends的理解_第2张图片

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 上界 不能往里存, 只能往外取.

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

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

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 下界不影响往里存,但往外取只能放在Object对象里

(1) 使用下界 会使从盘子里取东西的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子类型限定的通配符可以向泛型对象读取

你可能感兴趣的:(JavaSe基础)