Skip to main content

泛型中 extends 和 super 的区别?

泛型中 extends 和 super 的区别?

<? extends T><? super T>是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念
<? extends T> :是指 上界通配符(Upper Bounds Wildcards)
<? super T> :是指 下界通配符(Lower Bounds Wildcards)

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

使用泛型的过程中,经常出现一种很别扭的情况。比如按照题主的例子,我们有Fruit类,和它的派生类Apple类。
class Fruit {}
class Apple extends Fruit {}
然后有一个最简单的容器:Plate类。盘子里可以放一个泛型的“东西”。我们可以对这个东西做最简单的“放”和“取”的动作:set( )和get( )方法。
public class Plate<T> {
 private T item;
 public Plate(T item) {
   this.item = item;
 }
 public T getItem() {
   return item;
 }
 public void setItem(T item) {
   this.item = item;
 }
}
现在我定义一个“水果盘子”,逻辑上水果盘子当然可以装苹果。
Plate<Fruit> plate = new Plate<Apple>(new Apple());
但实际上Java编译器不允许这个操作。会报错,“装苹果的盘子”无法转换成“装水果的盘子”。
error: incompatible types: Plate<Apple> cannot be converted to Plate<Fruit>
所以我的尴尬症就犯了。实际上,编译器脑袋里认定的逻辑是这样的:
苹果 IS-A 水果
  • 装苹果的盘子 NOT-IS-A 装水果的盘子
  • 所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的。所以我们不可以把Plate的引用传递给Plate。
为了让泛型用起来更舒服,Sun的大脑袋们就想出了<? extends T>和<? super T>的办法,来让“水果盘子”和“苹果盘子”之间发生关系。

什么是上界?

下面代码就是“上界通配符(Upper Bounds Wildcards)”:
Plate<? extends Fruit>
翻译成人话就是:一个能放水果以及一切是水果派生类的盘子。再直白点就是:啥水果都能放的盘子。这和我们人类的逻辑就比较接近了。Plate<? extends Fruit>Plate<Apple>最大的区别就是:Plate<? extends Fruit>Plate<Fruit>以及Plate<Apple>的基类。直接的好处就是,我们可以用“苹果盘子”给“水果盘子”赋值了。
Plate<? extends Fruit> plate = 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> 覆盖下图中蓝色的区域。 

什么是下界?

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

上下界通配符的副作用

边界让Java不同泛型之间的转换更容易了。但不要忘记,这样的转换也有一定的副作用。那就是容器的部分功能可能失效。
还是以刚才的Plate为例。我们可以对盘子做两件事,往盘子里set()新东西,以及从盘子里get()东西。
public class Plate<T> { 
  private T item;
  public Plate(T item) {
    this.item = item;
  }
  public T getItem() {
    return item;
  }
  public void setItem(T item) {
    this.item = item;
  }
}

上界<? extends T>不能往里存,只能往外取

<? extends Fruit>会使往盘子里放东西的set( )方法失效。但取东西get( )方法还有效。比如下面例子里两个set()方法,插入Apple和Fruit都报错。
Plate<? extends Fruit> plate = new Plate<Fruit>(new Fruit());
//不能存入任何元素
plate.setItem(new Fruit()); //error
plate.setItem(new Apple()); //error

//读取出来的东西只能存放在Fruit或它的基类里。
Apple apple = plate.getItem(); //error
Fruit fruit = plate.getItem();
Food food = plate.getItem();
Object object = plate.getItem();
原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。
所以通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。
public <T> List<T> fill(T... t);
但通配符没有这种约束,Plate单纯的就表示:盘子里放了一个东西,是什么我不知道。
所以题主问题里的错误就在这里,Plate<? extends Fruit>里什么都放不进去。

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

使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。set( )方法正常。
Plate<? super Fruit> p = new Plate<Fruit>(new Fruit());

//存入元素正常
p.setItem(new Fruit());
p.setItem(new Apple());

//读取出来的东西只能存放在Object类里。
Apple newFruit3 = p.getItem();    //Error
Fruit newFruit1 = p.getItem();    //Error
Object newFruit2 = p.getItem();
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。

PECS原则

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

Comments

Popular posts from this blog

《股票大作手回忆录》杰西·利弗莫尔十大交易心法

  1、认清大势 第5章原文:赚大钱不能靠个股的波动,而要靠大盘走势;不能靠解读盘面,而要靠预判整个市场和市场趋势。 书中写了利弗莫尔赚大钱的几次关键机会:1907年先做空市场,底部时反手做多,赚取数百万美元;1915-1916年牛市做多,高位时做空12只股票,又赚回数百万美元;还有经典的1929年,做空赚了1亿美元。 而1911到1914年期间,市场没有赚钱机会,利弗莫尔说他一分钱都没有赚到。 2、交易心理是关键 第12章 原文 :当一个人急于让市场满足自己的需求时,他会怎么做?他是在赌博,所以他因此承受的风险远高于他理智时的操作。 1908年,利弗莫尔做棉花期货大亏,从几百万身家亏到只有几十万,为了扳平回本,他最后连几十万都全部亏完,还背了一身债,所以说财不入急门,赌性要不得。 第14章 原 文 :只要我欠着钱,我就会一直担心,我就无法顺利地东山再起,这一点明白无误,我告诉自己:我必须破产。 1915年,利弗莫尔欠了100多万美元的债务,但因为债务问题导致心理问题,无法在市场正常发挥,所以不得已破产来减轻债务,以轻装上阵。所以焦虑和压力的心理,是交易的大敌。 3、趋势为王 第10章 原 文 :价格和其他东西一样,沿着最小阻力方向运动,所有你需要做的就是观察行情走向,确定市场阻力位,一旦确定之后时刻准备好沿着最小 阻力线 交易。 利弗莫尔所说的最小阻力线,是指价格突破盘整区间的上下边,即突破了阻力位,表明行情的趋势已经走出来,可以跟随突破方向进行交易。 4、耐心等待交易机会 第14章 原 文 :六周的耐心等待之后,最终我还是用理智战胜了贪婪和欲望。 利弗莫尔买伯利恒钢铁的时候,只有一次赊来的交易机会,可以买500股,为了保证万无一失,他耐心等待了6周,等到这个股票整数突破100点的时候买入,打赢了他交易历史上最著名的翻身仗。 5、独立分析判断,拒绝小消息 第8章 原 文 :我变得比任何时候都更加独立,不会去听信别人的小道消息和评论,也不会理会其他人的意见、推测和怀疑。 利弗莫尔经历过多次失败,都是因为失去了独立思考和轻信他人的缘故。1907年轻信好友的干预损失4万美元,1908年又被洗脑做多棉花而遭遇惨败。所以他学会了反其道而行之,反向利用小道消息进行操作,均取得了盈利。 6、浮盈加仓 第7章 原文 :初次交易之后,除非能够显示你可以盈利,否则不要进行第二...

虚拟币交易平台 viabtc_exchange_server ubuntu 16.04 安装步骤

########1############ 下载宝塔: wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh Congratulations! Install succeeded! ================================================================== Bt-Panel: http://155.138.211.245:8888/37dfffff username: dbdiyxmj password: f2ec3b83 Warning: If you cannot access the panel, release the following port (8888|888|80|443|20|21) in the security group ================================================================== sudo apt update ########2############ sudo apt install libev-dev libjansson-dev libmpdec-dev libmysqlclient-dev libcurl4-gnutls-dev libldap2-dev libgss-dev librtmp-dev libsasl2-dev git default-jdk openssl libssl-dev mysql-server 数据库root 密码:root ########3############ wget http...

Publish an Android library to Maven with AAR ,android studio maven aar nexus 上传aar android上传aar到Maven Central 2022年最新最全

1. Github 1.1 注册github 点击下面链接进行注册 注册GitHub账号 2. 注册sonatype账号 2.1 注册 注册sonatype 输入 Email, Full name,Username,Password,输入验证码,最后点击Sign up就可以注册了。 required 2.2 登录 登录sonatype 2.3 新建group id 登录之后点击 新建 按照下图中的步骤一步一步来即可,最后别忘记点击新建。 2.4 github新建相关仓库 新建完成之后我们需要在github新建一个仓库,证明你是这个github的使用者(例如这个是我的 https://github.com/996dev) 等待审核通过之后,我们就可以往 mavenCentral 上传aar或者jar包了 3. GPG 3.1 下载软件 GPG官网下载地址 下载对应系统的文件 下载完成之后点击安装,直到完成。 3.2 生成密钥 创建密钥命令,在创建的时候一定要记住设置的密码,后面要用的 gpg --full-generate-key 导出 私钥,这个后面会用的到,使用下面命令进行导出 gpg --export-secret-keys -o secring.gpg 3.3 上传公钥到服务器 上传到这个三个地方 keyserver.ubuntu.com,pgp.mit.edu,keys.openpgp.org gpg --keyserver hkp://xxxx.xxx --send-keys YOUR KEY ID 3.3.1上传 使用下面命令进行上传 gpg --keyserver keyserver.ubuntu.com --send-keys YOUR KEY ID gpg --keyserver pgp.mit.edu --send-keys YOUR KEY ID gpg --keyserver keys.openpgp.org --send-keys YOUR KEY ID keys.openpgp.org 命令行出问题使用下面的网页直接上传 https://keys.openpgp.org/uploa 3.3.2 验证上传是否成功 使用下面的命令进行验证是否上传成功 gpg --keyserver keyserver.ubuntu.com --rec...