从设计角度搞懂PECS &<T>&<? extends T>&<? super T> 关系_s<? extends son>-程序员宅基地

技术标签: PECS  java  原理研究  

场景还原

一个java开发者在其开发的生涯中, 难免会写这样的代码

List<Son> ss = new ArrayList<>();
// 父类对象的集合引用子类对象的集合
List<Father> fs = ss;

也许你更多的遇到的是一个方法参数是List<Father>, 但是你传入了一个List<Son>进去, 但是无所谓了, 上面只是一个示例

然后编辑器就会爆红, 编译的时候也会抛出异常.

但是如果我们将代码写成下面的样子

List<? extends Son> ss = new ArrayList<>();
// 父类对象的集合引用子类对象的集合
List<? extends Father> fs = ss;

然后就发现, 编译正常通过, 运行也正常, 但是在我们在调用add() 方法, 往里面加对象时却发现, 编译器再次爆红

List<? extends Son> ss = new ArrayList<>();
// 父类对象的集合引用子类对象的集合
List<? extends Father> fs = ss;
// 编译不通过, 爆红
fs.add(new Father())

然后我们就特别不理解为什么会这样!

查找找原因

当我们上网查找原因时, 无非就是搜到

  1. 向上转型是安全的,向下转型是不安全的,除非你知道List中的真实类型,否则向下转型就会报错
  2. T? extends T? super T 的区别.
  3. 上界, 下界之类的概念
  4. 或是PECS之类的概念

    Remember PECS: “Producer Extends, Consumer Super”.
    “Producer Extends” - If you need a List to produce T values (you want to read Ts from the list), you need to declare it with ? extends T, e.g. List<? extends Integer>. But you cannot add to this list.
    “Consumer Super” - If you need a List to consume T values (you want to write Ts into the list), you need to declare it with ? super T, e.g. List<? super Integer>. But there are no guarantees what type of object you may read from this list.
    If you need to both read from and write to a list, you need to declare it exactly with no wildcards, e.g. List.


老实说, 看了上面的乱起八糟的东西, 我是一脸懵逼, 最终捣鼓研究之后才明白, 哦! 原来是这种情况.

虽然这个设计理念很合理, 但是对于开发者学习来讲, 特别不友好, 我觉得很多人初学这些概念的时候也和我一样都是一脸懵逼的状态.

为了方便大家的理解, 我们站在设计者的角度上去考虑这样设计的缘由.

从设计角度分析T? extends T

先来一个接口Fruit, 两个类 ApplePair 都是 Fruit 的实现类.

interface Fruit {
     }

class Apple implements Fruit {
     }

class Pair implements Fruit {
     }

我们都知道苹果是一个水果, 那么一箱苹果也是一箱水果

那么我们可以很自然的在java中这样设计;

因为苹果是一个水果

Apple apple = new Apple();
Fruit fruit = apple;

所以一箱苹果也是一箱水果

List<Apple> apples = new ArrayList<Apple>();
List<Fruit> fruits = (List<Fruit>) apples; // 假设这个是对的

可是这里就出现了一个问题

List<Apple> apples = new ArrayList<Apple>();我们需要把它看成两部分.
等号后面new ArrayList<Apple>() 这是一个苹果篮子的实例对象, 它只能够放苹果, 不能够往里面加梨.
等号前面是它的引用, 当这个苹果篮子的实例对象被List<Apple>类型引用时, 我们可以正常使用它, 我们可以通过add()方法往里面加苹果, 不能够往里面加梨.
但是当这个苹果篮子的实例对象被List<Fruit>类型引用时, 我们就可以往里面放Fruit, 那么我们既可以往里面放苹果, 也可以放梨.
然后就造成了一个设计上的bug, 我们往一个只能够放苹果的篮子里面放入了梨.

   public void test1() {
    
      // 水果篮子, 不仅可以放苹果, 还可以放梨
      List<Fruit> fruits = new ArrayList<>();
      // 一篮水果, 只能放苹果
      List<Apple> apples = new ArrayList<>();
      // 苹果是一个水果, 但是一箱苹果不是一箱水果
      // 当你把一篮苹果看成一篮水果, 那么这篮子水果里面岂不是可以放梨
      List<Fruit> fruitList = (List<Fruit>) apples;  // 编译错误
   }

那么这样设计肯定是不行的, 它有一个冲突

  1. 苹果是一个水果, 那么一箱苹果也是一箱水果, 这个是我们的生活常识, 但是java的语言特性让我们不能够将List<Apple>看成是List<Fruit>.
  2. 但是在很多情况下, 我们的确需要将一箱苹果看作是一箱水果.

为了解决上面的冲突, 单凭一个List<T> 肯定是不够的, 那么我们引入一个List<? extend T>.

我们可以这样
我们不能直接将List<Apple>看成是List<Fruit>,
但是我们可以将List<Apple>看作是List<? extend Apple>,
然后再将List<? extend Apple>看成是List<? extend Fruit>.

List<Apple> apples = new ArrayList<Apple>();
List<Fruit> fruits = apples; // error

List<? extend Apple> appleTs = apples;
List<? extend Fruit> fruitTs = appleTs;

我们将 List<Fruit> 看作是一个引用, 它引用的就是一箱水果(new ArrayList<Fruit>()), 没有其他的可能, 因此它里面可以放苹果, 可以放梨, 放桃子.
我们将 List<? extend Fruit> 看作是一个引用, 它引用的可能是一箱只能放苹果的篮子, 也可能是一个只能放水果的篮子,

  • 我们不能够确定它里面能够放什么东西, 因此我们干脆在设计的时候就禁止往它里面放东西.
  • 但是这里面却可以取东西, 因为不管这里面是一箱只能放苹果的篮子或是一个只能放水果的篮子, 从它里面取出来的必定是水果.

同理扩展分析T? super T

interface Fruit {
    
   default void fun1();
}

class Vegetable {
     
   void fun2(){
    }
}

class Tomato extends Vegetable implements Fruit {
    
}

对于一个Fruit实例对象来讲, 其中只有 fun1() 方法.
但是如果 List<? super Tomato> 中存入的可以看作是 Tomato, Tomato 既有 fun1() 方法, 也有fun2().
但是如果List<? super Tomato> 实际引用的对象是一个List<Vegetable>, 那么List<? super Tomato>通过get()获取到的"Tomato"对象实际上是一个Vegetable的实体类, 那么调用"Tomato"的fun1(), 实际上调用的是Vegetable中的 fun1(), 但是 Vegetable中没有fun1(), 因此会出现冲突.
List<? super Tomato> 中的实例我们不知道是什么, 但是只要是实例, 那么就是一个Object

因此<? super T>,set()方法正常,但get()只能存放Object对象里

List<Fruit> fruits = new ArrayList<>();
List<? super Tomato> list = fruits;

list.get(0).fun2(); // 获取到的是Fruit实例, 但是Fruit里面没有fun2()函数

结论

List<T> 之间是不能相互引用转换的(List<Fruit>List<Apple> 不能相互转换).
为了使集合模板相互转换, 我们可以先将 List<T> 变成 List<? extends T>List<? super T>, 然后进行引用转换.

实际上无论是方法传参还是引用赋值, 你会发现无非就是引用之间的关系转换.

我们抛开对象, 只看引用关系, 用代码表示那么就是下面的样子


    interface Fruit {
     }

    static class Apple implements Fruit {
     }

    public static void extendsTest(String[] args) {
    
        List<Apple> apples = null;
        List<Fruit> fruits = null;
        
        List<? extends Apple> appleExtends = null;
        List<? extends Fruit> fruitExtends = null;
        
        // List<Apple> 可以转化为 List<? extends Apple>
        appleExtends = apples;
        // List<Apple> 可以转化为 List<? extends Fruit>
        fruitExtends = apples;
        // List<? extends Apple> 可以转化为 List<? extends Fruit>
        fruitExtends = appleExtends;

        // 可以正常 get
        final Apple apple = appleExtends.get(0);
        // 添加对象特爆红, 编译不通过
        appleExtends.add(new Apple());  // error
    }


    public static void superTest(String[] args) {
    
        List<Apple> apples = null;
        List<Fruit> fruits = null;
        List<? super Fruit> fruitSupers = null;
        List<? super Apple> appleSupers = null;

        // List<Apple> 可以转化为 List<? extends Apple>
        appleSupers = apples;
        // List<Apple> 可以转化为 List<? extends Fruit>
        fruitSupers = fruits;
        // List<? extends Apple> 可以转化为 List<? extends Fruit>
        appleSupers = fruitSupers;

        // 添加对象正常
        appleSupers.add(new Apple());
        // 获取对象必须使用 Object 引用
        final Object object = appleSupers.get(0);
    }

转换为图片就是下面的关系

在这里插入图片描述
接下来看PECS是不是就很清晰了呢?

Remember PECS: “Producer Extends, Consumer Super”.
“Producer Extends” - If you need a List to produce T values (you want to read Ts from the list), you need to declare it with ? extends T, e.g. List<? extends Integer>. But you cannot add to this list.
“Consumer Super” - If you need a List to consume T values (you want to write Ts into the list), you need to declare it with ? super T, e.g. List<? super Integer>. But there are no guarantees what type of object you may read from this list.
If you need to both read from and write to a list, you need to declare it exactly with no wildcards, e.g. List.

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u011511756/article/details/112248611

智能推荐

蓝凌EIS智慧协同平台saveImg接口存在任意文件上传漏洞_蓝凌eis智慧协同平台文件上传漏洞-程序员宅基地

文章浏览阅读979次。蓝凌智慧协同平台eis集合了非常丰富的模块,满足组织企业在知识、协同、项目管理系统建设等需求。_蓝凌eis智慧协同平台文件上传漏洞

LLaVA-1.5-程序员宅基地

文章浏览阅读193次。与InstructBLIP或Qwen-VL在数亿甚至数十几亿的图像文本配对数据上训练的、专门设计的视觉重新采样器相比,LLaVA用的是最简单的LMM架构设计,只需要在600K个图像-文本对上,训练一个简单的完全连接映射层即可。结果表明,LLaVA-1.5不仅可以使用更少的预训练和指令微调数据,而且还可以利用最简单的架构、学术计算和公共数据集来实现最佳的性能——在12个基准中的11个上取得了SOTA。为了解决这个问题,研究人员建议在VQA问题的末尾,添加一个可以明确输出格式的提示,进而让模型生成简短回答。

ORACLE基本数据类型总结_oracle 数值类型最大值-程序员宅基地

文章浏览阅读442次。2013-08-17 21:04 by 潇湘隐者, 100246 阅读, 5 评论, 收藏, 编辑 ORACLE基本数据类型(亦叫内置数据类型 built-in datatypes)可以按类型分为:字符串类型、数字类型、日期类型、LOB类型、LONG RAW& RAW类型、ROWID & UROWID类型。在讲叙字符串类型前,先要讲一下编码。字符串类型的数据可依编码方式分成_oracle 数值类型最大值

10种机器学习算法_决策树和mlp-程序员宅基地

文章浏览阅读315次。作为数据科学家的实践者,我们必须了解一些通用机器学习的基础知识算法,这将帮助我们解决所遇到的新领域问题。本文对通用机器学习算法进行了简要的阐述,并列举了它们的相关资源,从而帮助你能够快速掌握其中的奥妙。▌1.主成分分析(PCA)/ SVDPCA是一种无监督的方法,用于对由向量组成的数据集的全局属性进行理解。本文分析了数据点的协方差矩阵,以了解哪些维度(大部分情况)/数据点(少数情况)更为重要,即它..._决策树和mlp

桥接模式的实现-程序员宅基地

文章浏览阅读148次。在这个示例中,我们使用std::shared_ptr来管理Implementor对象的生命周期,确保在不再需要时自动释放资源。通过智能指针的使用,我们避免了手动管理内存的复杂性,提高了代码的可靠性和可维护性。希望这个示例能帮助你理解如何使用智能指针来实现桥接模式。当使用智能指针来实现桥接模式时,我们可以利用std::shared_ptr或std::unique_ptr来管理对象的生命周期,确保资源的正确释放。

制造业敏感文件外发不安全?一招解锁更高效的加密方式!-程序员宅基地

文章浏览阅读440次,点赞11次,收藏8次。云盒子在制造业上有丰富的部署经验,在面向制造类企业的重要文件,可以通过审计、授权、文件加密进行多重保护,使得图纸文件、专利技术、采购订单等敏感数据等到有效保护,做到无处可泄,同时安全可靠,也不会对日常工作效率有影响 ,实现真正有效的企业文件保护的目的,达到既防止机密文件外泄和扩散,又支持内部知识积累和文件共享的目的。云盒子的加密方式是通过将本地文件数据上传到云盘进行统一加密存储,而不是对设备加密,通过【本地加密】+【云加密】双重组合下,不管用什么设备打开文件都受到管控,使管理者管理起来能够更高效。

随便推点

计算几何讲义——计算几何中的欧拉定理-程序员宅基地

文章浏览阅读188次。在处理计算几何的问题中,有时候我们会将其看成图论中的graph图,结合我们在图论中学习过的欧拉定理,我们可以通过图形的节点数(v)和边数(e)得到不是那么好求的面数f。 平面图中的欧拉定理: 定理:设G为任意的连通的平面图,则v-e+f=2,v是G的顶点数,e是G的边数,f是G的面数。证明:其实有点类似几何学中的欧拉公式的证明方法,这里采用归纳证明的方法。对m..._怎么证明平面图欧拉定理

c语言中各种括号的作用,C语言中各种类型指针的特性与用法介绍-程序员宅基地

文章浏览阅读750次。C语言中各种类型指针的特性与用法介绍本文主要介绍了C语言中各种类型指针的特性与用法,有需要的朋友可以参考一下!想了解更多相关信息请持续关注我们应届毕业生考试网!指针为什么要区分类型:在同一种编译器环境下,一个指针变量所占用的内存空间是固定的。比如,在16位编译器环境 下,任何一个指针变量都只占用8个字节,并不会随所指向变量的类型而改变。虽然所有的指针都只占8个字节,但不同类型的变量却占不同的字节数..._c语言带括号指针

缅甸文字库 缅甸语字库 缅甸字库算法_0x103c-程序员宅基地

文章浏览阅读9.5k次。字库交流 QQ:2229691219 缅甸语比较特殊、缅甸语有官方和民间之分,二者不同的是编码机制不同,因此这2种缅甸语的字串翻译、处理引擎、字库都是不同的。我们这里只讨论官方语言。 缅文、泰文等婆罗米系文字大多是元音附标文字,一般辅音字母自带默认元音可以发音,真正拼写词句时元音像标点符号一样附标在辅音上下左右的相应位置。由于每个元音位于辅音的具体位置是有自己的规则的,当只书写..._0x103c

Python+django+vue校园二手闲置物品拍卖系统pycharm毕业设计项目推荐_基于python+django+vue实现的校园二手交易平台-程序员宅基地

文章浏览阅读200次。在校园,随着学生数量的增多,存在许多生活和学习物品,许多学习用品经过一学期学习之后往往被闲置,一些出于一时喜欢而购买的物品使用机会少而被闲置,还有一些物品以低廉的价格卖给资源回收站,造成巨大的资源浪费。校园闲置物品拍卖系统使用python技术,MySQL数据库进行开发,系统后台使用django框架进行开发,具有低耦合、高内聚的特点,其中校园用户通过人脸识别的方法增加系统安全性,在闲置物品推荐中,使用协同过滤算法进行商品推荐。系统的开发,帮助高校有效的对闲置物品进行管理,提高了闲置物品销售的效率。_基于python+django+vue实现的校园二手交易平台

【推荐系统论文精读系列】(十)--Wide&Deep Learning for Recommender Systems_引用《wide & deep learning for recommender systems》-程序员宅基地

文章浏览阅读1.1k次,点赞3次,收藏3次。文章目录Wide & Deep Learning for Recommender Systems一、摘要二、介绍三、推荐系统综述四、Wide&Deep学习4.1 Wide部分4.2 Deep部分4.3 联合训练 Wide&Deep ModelPreferenceWide & Deep Learning for Recommender Systems一、摘要具有非线性特征转化能力的广义线性模型被广泛用于大规模的分类和回归问题,对于那些输入数据是极度稀疏的情况下。通过使用交_引用《wide & deep learning for recommender systems》

c++ sleep函数_Linux 多线程应用中如何编写安全的信号处理函数-程序员宅基地

文章浏览阅读171次。关于代码的可重入性,设计开发人员一般只考虑到线程安全,异步信号处理函数的安全却往往被忽略。本文首先介绍如何编写安全的异步信号处理函数;然后举例说明在多线程应用中如何构建模型让异步信号在指定的线程中以同步的方式处理。Linux 多线程应用中编写安全的信号处理函数在开发多线程应用时,开发人员一般都会考虑线程安全,会使用 pthread_mutex 去保护全局变量。如果应用中使用了信号,而且信号的产生不..._linux c++ sleep 不被中断

推荐文章

热门文章

相关标签