<? extends T> 及<? super T> 重温

本文针对泛型中<? extends T> 及<? super T>的主要区别及使用用途进行讨论.

作者尽量描述其原理,分析疑点. 希望对复习Java泛型使用,项目架构及日常使用有帮助

也是作者作为学习的加强记忆

编码例子背景

设定有一盘子(容器),可以存放物品,同时有食物,水果等可以存放在容器里面.

import com.google.common.collect.Lists; //引入guava的Lists工具方便生产List实例
class Food {
/** name*/
protected String name = "Food";
/** 打印食物名称*/
public void echo() {
System.out.println(name);
}
} class Fruit extends Food {} class Apple extends Fruit {
public Apple() {
name = "Apple";
}
} class Pear extends Fruit {
public Pear() {
name = "Pear";
}
} class Plate<T> {
private T item;
public Plate() {}
public Plate(T item) {
this.item = item;
}
public void set(T t) {
item = t;
}
public T get() {
return item;
}
/** 模仿处理泛型T实例*/
public void methodWithT(T t) {
//简单打印实例
System.out.println("methodWithT,T's is : " + t);
}
}

引出问题背景

现在,有两个容器,一个放水果(没说明放哪种),一个特指放苹果

Fruit fruit = new Fruit();
Apple apple = new Apple(); Plate<Fruit> fruitPlate = new Plate<>();
Plate<Apple> applePlate = new Plate<>();

现在对着两个容器进行一些常规操作,赋值/调用API

Fruit fruit = new Fruit();
Apple apple = new Apple(); // 父类容器,放置子类
// 此处是多态的提供的API
// apple is a fruit
fruitPlate.set(apple);
fruitPlate.methodWithT(apple); // 父类容器引用指向(被赋值)子类容器
// 父类容器与子类容器的关系,此处关注的是容器Plate这个类!!!
// 并没有像父子类中的继承关系(多态)!!!
// apple's plate is not a fruit's plate
// ERROR
// fruitPlate = applePlate; // 装水果的盘子无法指向装苹果
// ERROR
// applePlate = fruitPlate;// 明显错误,子类容器指向父类容器

初学Java的读者看到此处,心中必定会有疑问,难道装苹果的盘子不是装水果的盘子?很遗憾,使用来修饰泛型,编译器确实是这么认为的

所以,以上测试代码中,父类容器引用指向(被赋值)子类容器,编译报错,是跟多态上的认识是相反的结果

为了解决此类容器间'继承多态'问题,实现父子容器泛类引用指向,于是JDK提供了<? extends T><? supper T>

<? extends T>

<? extends T> : 上界通配符(Upper Bounds Wildcards),表示上界是T,用此修饰符修饰的泛类容器,可以指向本类及子类容器

示例如下:

/**
* <? extends T> 上界通配符(Upper Bounds Wildcards)
* <p>
* Plate <? extends Fruit> extendsFruitPlate 可以被 Plate <Fruit> 及 Plate <Apple> 赋值
*/
@Test
public void extendsTest() {
Fruit fruit = new Fruit();
Apple apple = new Apple(); Plate<Fruit> fruitPlate = new Plate<>();
Plate<Apple> applePlate = new Plate<>();
Plate<? extends Fruit> extendsFruitPlate ; // SUCCESS
// Plate<? extends Fruit>引用可以指向Plate<Fruit>以及Plate<Apple>
extendsFruitPlate = applePlate;
extendsFruitPlate = fruitPlate;
}

<? supper T>

<? supper T> : 下界通配符(Lower Bounds Wildcards),表示下界是T,用此修饰符修饰的泛类容器,可以指向本类及父类容器

示例如下:

/**
* <? supper T> 下界通配符(Lower Bounds Wildcards)
* Plate <? supper Fruit> superFruitPlate 可以被 Plate <Fruit> 及 Plate <Object> 赋值
*/
@Test
public void supperTest() {
Fruit fruit = new Fruit();
Apple apple = new Apple(); Plate<Fruit> fruitPlate = new Plate<>();
Plate<Apple> applePlate = new Plate<>();
Plate<Object> objectPlate = new Plate<>();
Plate<? super Fruit> superFruitPlate = new Plate<>(); // SUCCESS
// Plate<? super Fruit>引用可以指向Plate<Fruit>以及Plate<Object>
superFruitPlate = fruitPlate;
superFruitPlate = objectPlate; // ERROR
// superFruitPlate = applePlate; // <? supper Fruit>修饰的容器不能被子类容器赋值
}

以上例子说明,<? extends T><? super T>可以解决父子类泛型之间的引用问题,但同时,在使用被修饰的泛型容器的相关API,也做出了相关的调整.

可以说,这样的便利,是建立在一定的规则上,和付出一些代价的.可以肯定地是,这些限制规则,是符合多态的规则.理解后对我们工作编程上对编译器安全类型限制理解有一定的帮助

以下说明是相关调整的表现及这样的限定原因

<? extends T> 修饰的泛型其接口特点

<? extends T> 具体表现:

  1. 返回父类T的接口:调用这类型的接口返回的实例类型是父类T(这句结论说跟没说一样,理解起来特别容易.)
  2. 接收父类T的接口:这类型的接口均不可以再被调用

形象点,就如同网上绝大多数描述的一样:不能往里存,只能往外取

注:存与取,仅仅是一种表现形式,确切来说我认为是返回T接口(方法)及接收T接口(方法)更为准确

不能往里存含义是接收T接口不能再调用,否则编译异常

只能往外取含义是返回T接口可以正常使用,返回的实例类型就是T

代码表现如下:

/**
* <? extends T> 上界通配符(Upper Bounds Wildcards)
* 注意点: 只能获取,不能存放
*/
@Test
public void extendsAttentionTest() {
Fruit fruit = new Fruit();
Apple apple = new Apple();
Pear pear = new Pear(); Plate<? extends Fruit> extendsFruitPlate;
extendsFruitPlate = new Plate<Fruit>(fruit);
extendsFruitPlate = new Plate<Apple>(apple);
extendsFruitPlate = new Plate<Pear>(pear); // 以下ERROR代码,尝试调用接收泛型T的方法,均编译不过
// ERROR:
// extendsFruitPlate.set(fruit);
// extendsFruitPlate.set(apple);
// extendsFruitPlate.set(pear);
// extendsFruitPlate.set(new Object());
// extendsFruitPlate.methodWithT(fruit);
// extendsFruitPlate.methodWithT(apple);
// extendsFruitPlate.methodWithT(pear);
// extendsFruitPlate.methodWithT(new Object());
// 以上注释的错误代码,初学者也会有疑问,
// 为什么<Plate<? extends Fruit> extendsFruitPlate;这样装水果子类的盘子,
// 现在什么东西都不能放了?那我还要这个容器有什么用?这是不是跟思维惯性认知有点偏差? // SUCCESS
// 返回的是泛型T即Fruit,具体的实例是Pear类型
Fruit getFruit = extendsFruitPlate.get();
getFruit.echo();// 输出Pear // 接口测试
class ExtendsClass {
public void extendsMethod(List<? extends Fruit> extendsList) {
// ERROR:
// 出错原理同上,不能调用接收泛型T的方法
// extendsList.add(fruit);
// extendsList.add(apple);
// extendsList.add(new Object());
// SUCCESS
// 获取是父类,可以强转为子类再使用
Fruit getFruitByList = extendsList.get(0);
getFruitByList.echo();
}
}
List<Fruit> fruits = Lists.newArrayList(fruit);
List<Apple> apples = Lists.newArrayList(apple);
ExtendsClass extendsClass = new ExtendsClass();
// List<? extends Fruit> extendsList可以接收List<Fruit>/List<Apple>
extendsClass.extendsMethod(fruits);
extendsClass.extendsMethod(apples);
}

<? extends T> 相关限制的原因

Fruit fruit = new Fruit();
Apple apple = new Apple();
Pear pear = new Pear();
Plate<? extends Fruit> extendsFruitPlate;
extendsFruitPlate = new Plate<Fruit>(fruit);
extendsFruitPlate = new Plate<Apple>(apple);
extendsFruitPlate = new Plate<Pear>(pear);

编译器的理解: Plate<? extends Fruit> extendsFruitPlate 这个盘子 :

  • 你不能保证读取到 Apple ,因为 extendsFruitPlate 可能指向的是 Plate<Fruit>
  • 你不能保证读取到 Pear ,因为 extendsFruitPlate 可能指向的是 Plate<Apple>
  • 你可以读取到 Fruit ,因为 extendsFruitPlate 要么包含 Fruit 实例,要么包含 Fruit 的子类实例.
  • 你不能插入一个 Fruit 元素,因为 extendsFruitPlate 可能指向 Plate<Apple>Plate<Pear>
  • 你不能插入一个 Apple 元素,因为 extendsFruitPlate 可能指向 Plate<Fruit>Plate<Pear>
  • 你不能插入一个 Pear 元素,因为 extendsFruitPlate 可能指向 Plate<Fruit>Plate<Apple>

你不能往Plate<? extends T>中插入任何类型的对象,因为你不能保证列表实际指向的类型是什么,

你并不能保证列表中实际存储什么类型的对象,唯一可以保证的是,你可以从中读取到T或者T的子类.

所以,

  • 可以调用接收泛型T的方法的接口 extendsFruitPlate.get() 获取 Fruit 的实例
  • 却不能调用接收泛型T接口 extendsFruitPlate.set(fruit)添加任何元素
  • 也不能调用接收泛型T接口 extendsFruitPlate.methodWithT(fruit)处理任何对象

extendsFruitPlate指向 Plate<Pear> 时候, 调用extendsFruitPlate.set(fruit)extendsFruitPlate.methodWithT(fruit)调用等价于

Apple apple = new Apple();
Plate<Pear> pearPlate=new Plate<Pear>();
// 以下明显类型不相同,且不符合多态,导致类型转换异常
pearPlate.set(apple);
pearPlate.methodWithT(apple);

可以说,<? extends T> 修饰的泛型容器可以指向子类容器,是建立在不能调用接收泛型T的方法条件上的,否则运行时将可能产生类型转换异常

编译器总是往最安全的情况考虑,尽量把可能存在的问题在编译期间就反映出来.所以编译器在处理<? extends T> 修饰的泛型容器时候,干脆让这个容器得接收泛型T方法不能再调用了

<? super T> 修饰的泛型其接口特点

<? super T> 具体表现

  1. 返回父类T的接口:调用这类型的接口返回的类型是Object类型
  2. 接收父类T的接口:调用这类型的只能传入父类T及T的子类实例

形象点,就如同网上绝大多数描述的一样:不影响往里存,但是往外取只能放在 Object

注:存与取,仅仅是一种表现形式,确切来说我认为是返回T接口(方法)及接收T接口(方法)更为准确

不影响往里存含义:调用这类型的只能传入父类T及T的子类实例

往外取只能放在 Object含义:调用这类型的接口返回的类型是Object类型,可以通过强转手段转化为子类

代码表现如下:

/**
* <? supper T> 下界通配符(Lower Bounds Wildcards)
* 注意点: 取出是Object,存放是父类或子类
*/
@Test
public void superAttentionTest() {
Object object = new Object();
Food food = new Food();
Fruit fruit = new Fruit();
Apple apple = new Apple();
Pear pear = new Pear(); Plate<? super Fruit> superFruitPlate;
// 可以被 Plate<Object> , Plate<Food> ,Plate<Fruit> Plate<父类容器> 赋值
superFruitPlate = new Plate<Object>(object);
superFruitPlate = new Plate<Food>();
superFruitPlate = new Plate<Fruit>(); // SUCCESS
superFruitPlate.set(fruit);
superFruitPlate.set(apple);
superFruitPlate.set(pear);
superFruitPlate.methodWithT(fruit);
superFruitPlate.methodWithT(apple);
superFruitPlate.methodWithT(pear);
// ERROR:接收父类T的接口,当[不是 T或T的子类时],则编译异常
// superFruitPlate.set(food);
// superFruitPlate.set(object);
// superFruitPlate.methodWithT(food);
// superFruitPlate.methodWithT(object);
// 以上注释的错误代码,初学者也会有疑问,
// 为什么<Plate<? super Fruit> superFruitPlate;这样可以指向水果父类的盘子,
// 现在却只能放子类?这是不是跟思维惯性认知有点偏差? // 只能获取到Object对象,需要进行强转才可以进行调用相关API
Object supperFruitPlateGet = superFruitPlate.get();
if (supperFruitPlateGet instanceof Fruit) {
// 为什么需要 instanceof ?
// superFruitPlate可以指向Plate<Food>,获取出来实际是Food实例
Fruit convertFruit = (Fruit) supperFruitPlateGet;
convertFruit.echo();
} // 接口测试
class SuperClass {
public void supperMethod(List<? super Fruit> superList) { superList.add(fruit);
superList.add(apple);
superList.add(pear);
// ERROR:原因如上,调用method(T t)时候,当t[不是 T或T的子类时],则编译异常
// superList.add(object);
// superList.add(food);
Object innerObject = superList.get(0);
if (innerObject instanceof Fruit) {
// 为什么需要 instanceof ?
// 像这样:superFruitPlate 可以指向List<Object> objects,获取出来是Object
Fruit innerConvertFruit = (Fruit) innerObject;
innerConvertFruit.echo();
} else {
System.out.println("supperMethod:非Fruit,插入非本类或非子类:" + innerObject);
}
}
}
List<Object> objects = new ArrayList<>(Arrays.asList(object));
List<Food> foods = new ArrayList<>(Arrays.asList(food));
List<Fruit> fruits = new ArrayList<>(Arrays.asList(fruit));
List<Apple> apples = new ArrayList<>(Arrays.asList(apple));
List<Pear> pears = new ArrayList<>(Arrays.asList(pear));
SuperClass superClass = new SuperClass();
superClass.supperMethod(objects);
superClass.supperMethod(foods);
superClass.supperMethod(fruits);
// ERROR 原因同上,非Fruit及Fruit父类容器则编译不通过
// superClass.supperMethod(apples);
// superClass.supperMethod(pears);
}

<? super T> 相关限制的原因

Plate<? super Fruit> superFruitPlate;
// 可以被 Plate<Object> , Plate<Food> ,Plate<Fruit> Plate<父类容器> 赋值
superFruitPlate = new Plate<Object>(object);
superFruitPlate = new Plate<Food>();
superFruitPlate = new Plate<Fruit>();

编译器的理解: Plate<? super Fruit> superFruitPlate 这个盘子 ,

  • superFruitPlate 不能确保读取到 Fruit ,因为 superFruitPlate 可能指向 Plate<Object>Plate<Food>
  • superFruitPlate 不能确保读取到 Food ,因为 superFruitPlate 可能指向 Plate<Object>

所以,取出来必须是Object,最后需要则调用强转

  • 你不能插入一个 Food 元素,因为 superFruitPlate 可能指向 Plate<Fruit>
  • 你不能插入一个 Object 元素,因为 superFruitPlate 可能指向Plate<Fruit>Plate<Food>
  • 你可以插入一个 Fruit/Apple/Pear 类型的元素,因为 Fruit/Apple/Pear 类都是 Fruit,Food,Object的本类或子类

所以,从 superFruitPlate 获取到的都是Object对象,superFruitPlate 插入的都是Fruit的本类或本身

故有如下结论:

  • superFruitPlate 调用返回父类T的接口,获取到的都是 Object 对象;
  • superFruitPlate 调用接收父类T的接口,只能传入父类T及T的子类实例

当 superFruitPlate 指向 Plate ,

调用 superFruitPlate.set(food) 和

superFruitPlate.methodWithT(food)

调用等价于:

Plate<Fruit> pearPlate=new Plate<Fruit>();
Food food=new Food();
// 以下明显类型不相同,且不符合多态,导致类型转换异常
fruitPlate.set(food);
fruitPlate.methodWithT(food);

可以说,<? super T> 修饰的泛型容器可以指向父类容器,是建立在调用接收T的接口,只能传入T及T的子类实例条件上的,否则运行时将可能产生类型转换异常

编译器总是往最安全的情况考虑,尽量把可能存在的问题在编译期间就反映出来.所以编译器在处理<? super T> 修饰的泛型容器时候,干脆让这个容器得接收泛型T方法只能传入T及T的子类

PECS (Producter Extends, Consumer Super) 原则

以上原则来源两者主要区别,合理使用其优点,有种去其糟粕,取其精华的意思

  • <? extends T> : 可以获取父类,向外提供内容,为生产者角色
  • <? super T> : 可以调用接收/处理父类及子类的接口,为消费者角色

举一个JDK 中例子:

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest"); if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
// si 来源List<? super T> dest,向外提供内容,生产者
// di 来源List<? extends T> src,接收/处理类型T的本类或子类,消费者
di.set(si.next());
}
}
}

小结

至此,本文完结了.重温的时候,发现很多之前想当然的结论,并没有细细研究其中原因.现在理解起来是不会再次忘记的了.要记住的是需要理解编译器是怎么认为的而不是怎么从修饰符去片面理解

通篇显得有点啰嗦,至少作者认为把重点及原因说清楚了.以上如有不当之处敬请指正.

参考

本文例子来源主要有二,最精髓的地方是StackOverflow的链接的第一第二个回答

博客园 : RainDream : <? extends T>和<? super T>

Stackoverflow:Difference between<? super T>and<? extends T>in Java

<? extends T> 及 <? super T> 重温的更多相关文章

  1. 简单物联网:外网访问内网路由器下树莓派Flask服务器

    最近做一个小东西,大概过程就是想在教室,宿舍控制实验室的一些设备. 已经在树莓上搭了一个轻量的flask服务器,在实验室的路由器下,任何设备都是可以访问的:但是有一些限制条件,比如我想在宿舍控制我种花 ...

  2. 利用ssh反向代理以及autossh实现从外网连接内网服务器

    前言 最近遇到这样一个问题,我在实验室架设了一台服务器,给师弟或者小伙伴练习Linux用,然后平时在实验室这边直接连接是没有问题的,都是内网嘛.但是回到宿舍问题出来了,使用校园网的童鞋还是能连接上,使 ...

  3. 外网访问内网Docker容器

    外网访问内网Docker容器 本地安装了Docker容器,只能在局域网内访问,怎样从外网也能访问本地Docker容器? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Docker容器 ...

  4. 外网访问内网SpringBoot

    外网访问内网SpringBoot 本地安装了SpringBoot,只能在局域网内访问,怎样从外网也能访问本地SpringBoot? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装Java 1 ...

  5. 外网访问内网Elasticsearch WEB

    外网访问内网Elasticsearch WEB 本地安装了Elasticsearch,只能在局域网内访问其WEB,怎样从外网也能访问本地Elasticsearch? 本文将介绍具体的实现步骤. 1. ...

  6. 怎样从外网访问内网Rails

    外网访问内网Rails 本地安装了Rails,只能在局域网内访问,怎样从外网也能访问本地Rails? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Rails 默认安装的Rails端口 ...

  7. 怎样从外网访问内网Memcached数据库

    外网访问内网Memcached数据库 本地安装了Memcached数据库,只能在局域网内访问,怎样从外网也能访问本地Memcached数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装 ...

  8. 怎样从外网访问内网CouchDB数据库

    外网访问内网CouchDB数据库 本地安装了CouchDB数据库,只能在局域网内访问,怎样从外网也能访问本地CouchDB数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Cou ...

  9. 怎样从外网访问内网DB2数据库

    外网访问内网DB2数据库 本地安装了DB2数据库,只能在局域网内访问,怎样从外网也能访问本地DB2数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动DB2数据库 默认安装的DB2 ...

  10. 怎样从外网访问内网OpenLDAP数据库

    外网访问内网OpenLDAP数据库 本地安装了OpenLDAP数据库,只能在局域网内访问,怎样从外网也能访问本地OpenLDAP数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动 ...

随机推荐

  1. DL Practice:Cifar 10分类

    Step 1:数据加载和处理 一般使用深度学习框架会经过下面几个流程: 模型定义(包括损失函数的选择)——>数据处理和加载——>训练(可能包括训练过程可视化)——>测试 所以自己写代 ...

  2. Prometheus监控实战day1-监控简介

    福利 Prometheus监控实战PDF电子书下载 链接:https://pan.baidu.com/s/1QH4Kvha5g70OhYQdp4YsfQ 提取码:oou5 若你喜欢该资料,请购买该资料 ...

  3. 史上最全的中高级Java面试题汇总

    原文链接:https://blog.csdn.net/shengqianfeng/article/details/102572691 memcache的分布式原理 memcached 虽然称为 “ 分 ...

  4. 【maven学习】构建maven web项目

    Maven Web应用 创建Web应用程序 要创建一个简单的java web应用程序,我们将使用Maven的原型 - web应用插件.因此,让我们打开命令控制台,进入到C: MVN目录并执行以下命令m ...

  5. CMDB资产采集的四种方式

    转 https://www.cnblogs.com/guotianbao/p/7703921.html 资产采集的概念 资产采集的四种方式:Agent.SSH.saltstack.puppet 资产采 ...

  6. 将笔记本无线网卡链接wifi通过有线网卡共享给路由器

    1.背景 背景这个就说来长了,在公司宿舍住着,只给了一个账号,每次登录网页都特别麻烦(需要账号认证那种).然后每个账号只支持一个设备在线,这就很尴尬了,那我笔记本.手机.Ipad怎么办? 当然,这时候 ...

  7. JVM性能调优的6大步骤,及关键调优参数详解

    JVM性能调优方法和步骤1.监控GC的状态2.生成堆的dump文件3.分析dump文件4.分析结果,判断是否需要优化5.调整GC类型和内存分配6.不断分析和调整JVM调优参数参考 对JVM内存的系统级 ...

  8. Spring 学习指南 第三章 bean的配置 (未完结)

    第三章 bean 的配置 ​ 在本章中,我们将介绍以下内容: bean 定义的继承: 如何解决 bean 类的构造函数的参数: 如何配置原始类型 (如 int .float 等) .集合类型(如 ja ...

  9. php中让数组顺序随机化,打乱顺序等

    php中有很多排序的函数,sort,rsort,ksort,krsort,asort,arsort,natcasesort,这些函数用来对数组的键或值进行这样,或那样的排序. 可以终究有时候还需要一些 ...

  10. python学习-31 内置函数

    内置函数 1.abs()  绝对值 2.all()    判断列表里的所有值的布尔值(如果迭代列表里的每个值后都是True 则返回True) '])) 运行结果: True Process finis ...