Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同?

Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同?

// compile error
// List <? extends Fruit> appList2 = new ArrayList();
// appList2.add(new Fruit());
// appList2.add(new Apple());
// appList2.add(new RedApple());

List <? super Fruit> appList = new ArrayList();
appList.add(new Fruit());
appList.add(new Apple());
appList.add(new RedApple());
关注者
428
被浏览
39,702
16 个回答
题主说的<? extends T>和<? super T>是Java泛型中的“通配符(Wildcards)”“边界(Bounds)”的概念。
  • <? extends T>:是指 “上界通配符(Upper Bounds Wildcards)”
  • <? super T>:是指 “下界通配符(Lower Bounds Wildcards)”

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<Apple>(new Apple());

但实际上Java编译器不允许这个操作。会报错,“ 装苹果的盘子”无法转换成“ 装水果的盘子”。
error: incompatible types: Plate<Apple> cannot be converted to Plate<Fruit>

所以我的尴尬症就犯了。实际上,编译器脑袋里认定的逻辑是这样的:
  • 苹果 IS-A 水果
  • 装苹果的盘子 NOT-IS-A 装水果的盘子

所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的。所以我们不可以把Plate<Apple>的引用传递给Plate<Fruit>

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

2. 什么是上界?
下面代码就是 “上界通配符(Upper Bounds Wildcards)
Plate< extends Fruit>

翻译成人话就是: 一个能放水果以及一切是水果派生类的盘子。再直白点就是: 啥水果都能放的盘子。这和我们人类的逻辑就比较接近了。Plate<? extends Fruit>和Plate<Apple>最大的区别就是: Plate<? extends Fruit>是Plate<Fruit>以及Plate<Apple>的基类。直接的好处就是,我们可以用“ 苹果盘子”给“ 水果盘子”赋值了。
Plate<? extends Fruit> p=new Plate<Apple>(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 Meat{}
class Beef extends Meat{}

//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}

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

3. 什么是下界?
相对应的, “下界通配符(Lower Bounds Wildcards)
Plate< super Fruit>

表达的就是相反的概念:一个能放水果以及一切是水果基类的盘子Plate<? super Fruit>是Plate<Fruit>的基类,但不是Plate<Apple>的基类。对应刚才那个例子,Plate<? super Fruit>覆盖下图中红色的区域。

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

边界让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;}
}

4.1 上界<? extends T>不能往里存,只能往外取
<? extends Fruit>会使往盘子里放东西的set( )方法失效。但取东西get( )方法还有效。比如下面例子里两个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<Apple>赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。

所以通配符<?>和类型参数<T>的区别就在于,对编译器来说 所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。
public <T> List<T> fill(T... t);

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

所以题主问题里的错误就在这里,Plate<? extends Fruit>里什么都放不进去。

4.2 下界<? super T>不影响往里存,但往外取只能放在Object对象里
使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。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对象才能装下。但这样的话,元素的类型信息就全部丢失。

5. PECS原则
最后看一下什么是 PECS(Producer Extends Consumer Super)原则,已经很好理解了:
  1. 频繁往外读取内容的,适合用上界Extends。
  2. 经常往里插入的,适合用下界Super。

首先,泛型的出现时为了安全,所有与泛型相关的异常都应该在编译期间发现,因此为了泛型的绝对安全,java在设计时做了相关的限制:

List<? extends E>表示该list集合中存放的都是E的子类型(包括E自身),由于E的子类型可能有很多,但是我们存放元素时实际上只能存放其中的一种子类型(这是为了泛型安全,因为其会在编译期间生成桥接方法 <Bridge Methods>该方法中会出现强制转换,若出现多种子类型,则会强制转换失败),例子如下:
List<? extends Number> list=new ArrayList<Number>();
        list.add(4.0);//编译错误
        list.add(3);//编译错误

上例中添加的元素类型不止一种,这样编译器强制转换会失败,为了安全,Java只能将其设计成不能添加元素。

虽然List<? extends E>不能添加元素,但是由于其中的元素都有一个共性--有共同的父类,因此我们在获取元素时可以将他们统一强制转换为E类型,我们称之为get原则。

对于List<? super E>其list中存放的都是E的父类型元素(包括E),我们在向其添加元素时,只能向其添加E的子类型元素(包括E类型),这样在编译期间将其强制转换为E类型时是类型安全的,因此可以添加元素,例子如下:
 List<? super Number> list=new ArrayList<Number>();
        list.add(2.0);
        list.add(3.0);

但是,由于该集合中的元素都是E的父类型(包括E),其中的元素类型众多,在获取元素时我们无法判断是哪一种类型,故设计成不能获取元素,我们称之为put原则

实际上,我们采用extends,super来扩展泛型的目的是为了弥补例如List<E>只能存放一种特定类型数据的不足,将其扩展为List<? extends E> 使其可以接收E的子类型中的任何一种类型元素,这样使它的使用范围更广。

List<? super E>同理。

省去术语,目的是让读者先明白。

java是单继承,所有继承的类构成一棵树。
假设A和B都在一颗继承树里(否则super,extend这些词没意义)。
A super B 表示A是B的父类或者祖先,在B的上面。
A extend B 表示A是B的子类或者子孙,在B下面。

由于树这个结构上下是不对称的,所以这两种表达区别很大。假设有两个泛型写在了函数定义里,作为函数形参(形参和实参有区别):

1) 参数写成:T<? super B>,对于这个泛型,?代表容器里的元素类型,由于只规定了元素必须是B的超类,导致元素没有明确统一的“根”(除了Object这个必然的根),所以这个泛型你其实无法使用它,对吧,除了把元素强制转成Object。所以,对把参数写成这样形态的函数,你函数体内,只能对这个泛型做插入操作,而无法读

2) 参数写成: T<? extends B>,由于指定了B为所有元素的“根”,你任何时候都可以安全的用B来使用容器里的元素,但是插入有问题,由于供奉B为祖先的子树有很多,不同子树并不兼容,由于实参可能来自于任何一颗子树,所以你的插入很可能破坏函数实参,所以,对这种写法的形参,禁止做插入操作,只做读取


具体请看 《effective java》里,Joshua Bloch提出的PECS原则
java - What is PECS (Producer Extends Consumer Super)?

我说一下我的理解,给题主做一个参考。
首先,Java有泛型这一个概念,是为了初衷是为了保证在运行时出现的错误能提早放到编译时检查。有了这个前提,再来看看题主的问题。

假设现在有这么一个类的继承树,Plant -> Fruit -> Apple -> RedApple。

List<? extends Fruit> appList2的意思,是一个列表,这个列表里面的元素是Fruit的某个子类T,那么在从appList2中取出一个元素时,编译器会自动把这个元素转型为T。那么现在假设T是RedApple,很显然你往这样一个appList2中add一个Apple类型的元素,取出后转型为RedApple必然会失败;同样的情况如果T是Apple,你add一个Fruit类型的元素,取出后转型为Apple,也会抛出异常。也就是说,编译器会把列表中取出的元素转型为某种类型,但编译器不确定这种转型是不是会成功,即在不保证运行时能顺利进行,因此就不允许你add任何类型的元素。

再来看看List<? super Fruit> appList,这个列表的元素都是Fruit的父类T,也就是说当你从这个列表中get一个元素时,编译器会自动加一句转型为T的代码。好,现在你往appList中add一个Apple类型的元素,取出时转型为T,由于T是Apple的父类,向上转型没有问题;加一个RedApple类型的元素,也一样不会有问题。也就是说,只要保证你往appList中添加的元素是Fruit的子类,编译器就可以保证在转型为T时不会抛出异常。因此第二种写法可以过编译。

List :”存的时候只能选一个类型。“
List <? extends Fruit> 意思: List中所有元素都是Fruit的子类(包含本身),
List <? super Fruit> 意思: List中所有元素都是Fruit的父类(包含本身)
1、List <? extends Fruit>
假设:Fruit有子类A、B、C 那么 list.add(A);list.add(B);list.add(C);显然错误(不能存多个类)。

虽然我们现在看的是ABC3个类就会问为什么会把不同类型的存进去,我这样存不就好了。list.add(A);list.add(A);其实这也是错误的,因为在运行之前他可不知道你到底add进去的东西是什么类型,是一样还是不一样,因实例化的时候是 ? 待定。为了避免类型不同的情况,所以会编译不通过。


2、List <? super Fruit>
假设:Fruit有子类A、B、C 那么 list.add(A);list.add(B);list.add(C); 这却是可以的,为什么呢:

因为他是这么存的:list.add((Fruit)A);list.add((Fruit)B); 自动强转了。因为 小转大是隐性的,大转小才是强转需要加类型。
那这里为什么又不能存Fruit的父类呢? 因为 见假设1,它是?号,类型代表待定,不跑起来他也不知道你到底存的什么。所以我们能手动add()进去的数据都必须是 绝对安全的(最低级父类:本身)才能通过。所以直接add父类也是不行的。

假设题主已经明白多态机制

List<?>,首先?号即无限制通配符类型,代表你不知道或者不关心泛型具体是什么类型。
List<? extends Fruit>,代表你只知道这个泛型的类型是Fruit子孙类,具体是哪一个你暂时还是不知道的。
List<? super Fruit>,代表你只知道这个泛型的类型是Fruit祖先类,具体是哪一个你暂时还是不知道的。

为什么List<? extends Fruit>无法add操作呢,假设Fruit下面还有一个Apple子类,而你的List实例是酱紫的:List<? extends Fruit> list = new ArrayList<Apple>(),这一刻你终于知道了泛型的具体类型是Apple类。
这样还可以add进Fruit吗?

而List<? super T>为什么又能add进呢,因为你add进的实例一定会是Fruit及其子类型,而你的List实例的实际泛型参数一定是Fruit及其父类,那么肯定是能add进去的。

关于这两个的使用,假设你是想消费这个List就该用List<? extends T>,因为你可以进行get操作,你可以调用T的接口方法;假设你是想被这个List消费就该用List<? super T>,因为你可以进行add操作。

我也不清楚我上面的术语表示的准不准确,反正是那么一回事- -
一个是父类型一个是子类型。但都表示某个特定类型,所以第一个会编译出错,因为编译器不知道会是哪个子类,那cast就有可能出错。第二个不会错因为子类cast成为父类的父类肯定没有问题

我从理论层面来说一气,需要那么一点点交换代数关于collection, functor的初步知识即可。

java container其实在泛型的情况是个functor,而其对应collection则是java几乎所有object,morphism则是所有的父子继承关系(issuper, ischild) 。

但是如果不用? extends, super, 这个functor 会变成invariant,也就是 issuper, ischild函数会变成无关性,我画个图:

A ----(ischild)---> B

| | Functor C (container) mapping

C(A) ischild (X) C(B)

这个functor是 invariant的,是不够“自然”的。

而 放入? wildcard 后变成:

A ----(ischild)-----> B

| | Functor C (container) mapping

C(A) --- ischild ---> C(? extends B)

这个是covariant的。

因为covariance是自然界非常自然的状态,所以泛型既要限制编译时期的typesafe,又要保证程序语言的自然属性,这点在polymorphism性质上特别重要。这也就是解释了上面很多答案为什么在polymorphism时候要大量使用? extends, super.

我觉得是这样的
首先一个要理解的是 从基类到子类 限制条件是逐渐严格的 比如 object 可以没有任何限制 》 fruit 就会比object多一些具体的限制 》 apple 又会比fruit多一些具体的限制

那泛型是干啥用的呢?我理解 他就是强类型语言 心里纠结下一个奇葩的产物 就是为了即想限制一下参数的类型 又不想为各种类型写一个重载(apple写一个 banana再写一个) 搞得自己很尴尬

首先说最难理解的添加 添加就要执行最严格的限制 如果一个范型可能是 object 也可能是fruit 还可能是 apple 那就要执行 apple的限制 保证 虽然我不知道是啥类型 但肯定按apple去卡 总不会犯错 就是宁可错杀一千 不能放过一个的傻叼思想,那说到这 添加时你就得想法告诉编译器 最严格的限制是啥 如果 是 ? extents fruit 那编译器是没法知道你的下限的 我卡住了你的fruit但是卡不住你想添加个apple的心阿 ,所以你就得写 ? extents fruit 告诉编译器 行了 别纠结了 你就按 fruit 给劳资卡就行了 其他的你就甭管了 就是说 哪怕你是一个ArrayList<object> 那往里添加一个apple 并按apple 去检查 那总是不会有错的

再说简单一些的取出 好吧 我编不下去了,我是今年才开始学java啊。。。。如果 ? extents fruit 只能添加 fruit 或者它的子类 那为毛 取出的时候 你不能给我转型到fruit而是转型到了 object ,好吧这就是另一个装逼了 虽然你不能添加 一个object 进一个 ArrayList<? super Fruit> 但是你可以整体赋值啊,所以编译器还是不敢说 你的list就是个fruit 而不是别的 其实说真的 这里我理解的很low 望大牛指正
class Fruit{}
class Apple extends Fruit{}
public class Client
{
    public static void main(String[] args) {
        ArrayList<? super Fruit> list = new  ArrayList<Object>(Arrays.asList(new Object(),new Object()));
        list.add(new Apple());
        //list.add(new Object()); //类型转换错误
        System.out.printf(list.toString());
    }
}

恩以上就是菜鸟对泛型这种装逼姿势的理解

想稍微补充一下。最高票答案挺不错,但基本所有答案都是回答了“是什么”,而没有说“为什么”。那么,java的compiler为啥不允许写一个covariant的type呢?为啥Java的List是invariant的呢?为啥Scala的List就是covariant的呢?

在Java 5引入generics之前,array已经是covariant了。covariant用array来解释,就是:

Apple是Fruit的subtype,那么Apple[]就是Fruit[]的subtype。 那么问题来了:

Apple[] aa = new Apple[]{new Apple()};
Fruit[] fa = aa;  //可以,Apple[]是Fruit[]的subtype
fa[0] = new Banana(); //可以,fa明明是Fruit[]的嘛

可以编译但运行会出错

Exception in thread "main" java.lang.ArrayStoreException: Banana

这就是楼上故事的开端。

Scala的List是immutable,自然也不会有写入的问题。凡事都要举一反三嘛。

? future T 不能取数据,只能存数据,那么数据给进去后,不能取出来,那有什么用?

边界,用于写入T,因为一定能够被想上转型为?对应的类型

super...向上限定,泛型可以是E及其父类。参考map集合构造传入一个comparator比较器,父类map重写了比较器方法的话,子类可以直接使用,不需要再定义一个比较器。

extends...向下限定,可以是E及其子类。见于集合addAll方法传的参数,把a集合添加到b个集合的时候,a里面的元素必须是b或者b的子类。

看了目前所有答案,恕我冒昧,都不是特别清楚。没人提到通配符的捕获,那么这个问题是很难解释明白的。占坑先邀请一波R大

直接上一个我感觉讲的比较好的博客(这个系列貌似都挺不错的,作者Brian Goetz自称
Oracle Java语言架构师):Java 理论与实践: 使用通配符简化泛型使用
英文原文:Java theory and practice: Going wild with generics, Part 1

博文是基于Java 5的,最后一段讲类型推断那点对于Java 7来说已经过时了,但丝毫不影响整篇文章的质量。代码清单3和清单4非常有趣,可以仔细看看。
可以看看这篇文章:Java语言中的协变和逆变
我的理解:
假设Apple继承Fruit,Orange继承Fruit,Fruit实现了Comparable<Fruit>想要把几个Fruit子类对象放入TreeMap比较
解析:V extends Comparable<? super V>
Apple extends Comparable<Fruit super Apple>,所以Apple不做改动就可以参与比较
写成这样是因为只要一个类的父类实现了Comparable<>,这个子类就可以使用,不需改动
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值