<? 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. System.Threading.Timer定时器使用注意事项

    1.定时器不要直接在方法里面定义和赋值,因为方法执行完,方法体内的变量会被GC回收. 有时候我们将timer定义在了方法里面,然后看到timer被执行了几次之后才失效,原因就是GC不一定会立即回收. ...

  2. rxjs debounceTime减少搜索的频率

    debounceTime用来降低事件的触发频率 ,接收以毫秒为单位的参数 它所做的操作是,在一定时间范围内不管产生了多少事件,它只放第一个过去,剩下的都将舍弃 html: <div class= ...

  3. 初识阿里开源的本地Java进程监控调试工具arthas(阿尔萨斯)

    转载自:https://www.cnblogs.com/linhui0705/p/9795417.html 上个月,阿里开源了一个名为Arthas的监控工具.恰逢近期自己在写多线程处理业务,由此想到了 ...

  4. [转帖]Stack Overflow上188万浏览量的提问:Java 到底是值传递还是引用传递?

    Stack Overflow上188万浏览量的提问:Java 到底是值传递还是引用传递? http://www.itpub.net/2019/12/03/4567/   在逛 Stack Overfl ...

  5. [转帖]亚马逊发布自主64核心ARM处理器:单核性能远超铂金至强

    亚马逊发布自主64核心ARM处理器:单核性能远超铂金至强 https://news.mydrivers.com/1/660/660383.htm 不知道真假 看样子比华为的鲲鹏920 要牛B . 亚马 ...

  6. UML部署图

    部署图与组件图密切相关,部署图是用来描述软件组件部署的硬件组件:而组件图是用来描述组件和显示了它们是如何在硬件中部署. 部署图通常用来帮助理解分布式系统,一个系统模型只有一个部署图. 部署图用于可视化 ...

  7. Android Studio代码错误提示无效(not available in Power Save mode)

    针对一位博友提的问题,我这边写出来,估计还是很多人会碰到这个问题,但是不知道如何解决的. 就是在设置了代码自动提示功能后,发现不生效的,如何设置代码自动提示请戳这:Android Studio如何设置 ...

  8. Oracle解决锁表语句与批量生成解锁语句

    --以下几个为相关表SELECT * FROM v$lock;SELECT * FROM v$sqlarea;SELECT * FROM v$session;SELECT * FROM v$proce ...

  9. 矩量母函数(Moment Generating Function,mgf,又称:动差生成函数)

    在统计学中,矩又被称为动差(Moment).矩量母函数(Moment Generating Function,简称mgf)又被称为动差生成函数. 称exp(tξ)的数学期望为随机变量ξ的矩量母函数,记 ...

  10. python-tyoira基本

    目录 .Typora安装 我们在之前的时候记录笔记就是使用word和记事本,但是从今天开始我们要更换软件,记录笔记使用Typora软件,为什么要使用Typora的软件呢,是因为我们程序员不只是写代码这 ...