目录

  • 泛型中的PECS原则以及使用注意
    • 一、泛型中的型变(协变、逆变、不可变)
      • 1. 什么是型变
      • 2. 什么是协变(Covariance)
      • 3. 什么是逆变(Contravariance)
      • 4. 不可变性(Invariance)
      • 5. 总结
    • 二、结合泛型的型变理解List、List\<?\>、List\< Object\>
      • 1. 关于泛型使用的定义
      • 2. 容器类使用泛型的好处
      • 3. 不应该使用原生类型的原因如下
      • 4. List、`List<?>`、`List`的区别
    • 三、开发中遇到的问题
      • Object参数与`List`参数的方法调用哪一个?
      • V... values与`Collection values`的区别
        • Java可变参数
        • 可能出现的问题
      • V... values的含义
      • `Collection values`的含义

泛型中的PECS原则以及使用注意

一、泛型中的型变(协变、逆变、不可变)

1. 什么是型变

 Object a = new String("ABCDEFG");

String 作为Object的子类,可以直接将子类对象赋值给父类。这个操作既达到了型变
但是使用泛型类型时,是无法型变的。
例子:

 List<String> strs = new ArrayList<>();// 这下面一行代码将会编译器报错,让我们避免后续的运行时异常List<Object> objs = strs;// 假设上一步的操作是被允许的, 紧接着将一个整数放入一个字符串列表objs.add(1);// 这里取出的时候将会报异常,ClassCastException 无法将整数转化为字符串String s = strs.get(0);

Java为了保证运行时安全,所以禁止了这样的操作。
实际开发中,开发者是需要语言对泛型类型的型变的支持。所以引出了协变逆变不可变的实现思想。
以此来支持泛型的型变
开始之前,先做两个准备工作

  1. 准备几个类并明确继承关系,每向右缩进一次,作为一次子类实现。

    • Fruit

      • Banana
      • Apple
        • RedApple
        • GreenApple
  2. 准备两个支持泛型的类Orchard(果园模拟产出角色:输出),shop(店铺模拟处理角色:输入)

     public static class Orchard<T> {T t;public void set(T t) {this.t = t;}public T get() {return t;}}public static class Shop<T> {T t;public void set(T t) {this.t = t;}public T get() {return t;}}

2. 什么是协变(Covariance)

概念:一个类型规则或者类型构造器保持了子继承父的类型关系,即:子类型 <= 父类型
作用:能够接收比原始指定的派生类型的派生程度更具体的类型
实际开发中,需要子泛型类型能够型变为父泛型类型
直觉上的实现代码:

 Orchard<Fruit> orchardF = new Orchard<Apple>();

这当然是无法通过编译的,需要扩大左边类型的赋值范围,需要通配符<? extend X>来接收更具体的类型范围

   Orchard<? extends Fruit> orchardF = new Orchard<Apple>();

扩大了接收类型为Fruit及其子类。依然维持读取类型为Fruit,不管协变类型是哪个Fruit子类,读取Fruit都是类型安全的。

 Orchard<? extends Fruit> orchardF = new Orchard<Apple>();       Fruit fruit = orchardF.get();

不可以进行写入,以下代码将报错:

 Orchard<? extends Fruit> orchardF = new Orchard<Apple>();orchardF.set(new Apple());

在使用<? extends E>的泛型集合中,对于元素的类型,编译器只知道元素是继承自E,具体是E的那个子类,这是无法知道的,所以向一个无法知道具体类型的泛型集合中插入元素是不能通过编译的。但是,由于知道元素是继承自E,所以从这个泛型集合中取Fruit类型的元素是可以的。

Java中的协变:在Java中通常将通配符? extends E限制上边界来完成协变(限定输出类型的上边界)。保证了接收类型协变,读取类型安全。

值得注意的是,类型变量的上限可以为多个,必须使用&符号相连接,例如 List<T extends Number & Serializable>;其中,&必须为接口

3. 什么是逆变(Contravariance)

概念:一个类型规则或者类型构造器逆转了子继承父的类型关系。即:以父类型接受最后输出子类型
作用:能够接收比原始指定的派生类型的派生程度更小(不太具体)的类型。
在进行消费操作时如果需要使用父类型的操作,这需要下边的实现

  Shop<Apple> shopA = new Shop<Fruit>();

这也是不被允许的,需要使用<? super X>通配符来扩大接收类型范围,来接收实际操作对象是泛型为父类型Fruit的具体对象。

 Shop<? super Apple> shopA = new Shop<Fruit>();

这样似乎看起来有些问题,扩大了类型接收范围,也扩大输入范围,因为声明类型默认是以实际泛型类型作为上边界的,像下面这样:

 Shop<Fruit> shopF = new Shop<Fruit>();shopF.set(new Apple());shopF.set(new Banana());

使用了父类型,同时又被迫扩大了输入范围(期望只输入Appel及其子类),这显然不正确。
这便是**<? super E>通配符的作用所在,扩大赋值类型范围,限制输入类型范围**,称之为限制下边界。
输入例子:

 Shop<? super Apple> shopA = new Shop<Fruit>();shopA.set(new Apple());shopA.set(new RedApple());shopA.set(new Banana());//编译错误,Banner不是Apple的子类

关于下边界的理解,在上面的例子中:<? super Apple>将Apple指定为对象Shop<Fruit>()的输入类型的下边界。

在使用<? super E>的泛型集合中,元素类型是E的父类,但无法知道是那个具体的父类,因此读取时无法确定以某个具体的父类进行读取(只能以Object读取),可以插入Appel与Appel的子类,因为这个集合中的元素都是Apple的父类。(意思就是插入Appel与他的子类后,可以安全的转为Appel父类的引用)

Java中的逆变:在Java中通常将通配符<? super E>限制下边界,允许声明的泛型为E的父类对象,完成逆变,限定了输入类型的下边界,从输入处隔绝了类型不安全。

4. 不可变性(Invariance)

概念:不满足协变的同时不满足逆变即为不可变

不使用通配符的情况下都是不可变的。

 Shop<Fruit> shopF = new Shop<Fruit>()

5. 总结

泛型集合的使用遵从PECS原则

PECS 代表生产者——Extends、消费者——Super (Producer----Extends, Consumer ---- Super)
只读不可写时,使用List<? extends Fruit>:Producer
只写不可读时,使用List<? super Apple>:Consumer

二、结合泛型的型变理解List、List<?>、List< Object>

1. 关于泛型使用的定义

定义:声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型类或者接口。泛型类和接口统称为泛型(generic type)。

每种泛型定义一组类型参数(formal type parameters),这些类型形参也被简称为类型参数(type parameter),例如对于泛型(generic type)List而言,List就是一个参数化的类型,String就是对应于类型参数类型实参(actual type parameter)

每个泛型定义一个原生类型(raw type),即不带任何类型参数的类型名称,例如,与List对应的原生类型是List。原生类型就像从类型声明中删除了所有泛型信息一样。实际上原生类型List与Java平台在所有泛型之前的接口类型List完全一样。

2. 容器类使用泛型的好处

  • 安全性:在对参数化类型的容器中放入了错误即不匹配的类型的时候,编译器将会强制性进行错误提示。
  • 便利性:当从容器中取出元素的时候不用自己手动将Object转换为元素的实际类型了,编译器将隐式地进行自动转换。
  • 表述性:带有类型实参的泛型即参数化类型,可以让人看到实参就知道里面的元素E都是什么类型。

3. 不应该使用原生类型的原因如下

虽然使用原生类型是合法的,但不提倡这样做,因为如果使用原生类型,就丢失了泛型在安全性和表述性方面的优势

安全性:比如可能不小心把一个java.util.Date实例错误地放进一个原本包含java.sql.Date实例的集合当中,虽然在编译期不会出现任何错误,但在运行期一旦尝试类型转换就会发生ClassCastException,而泛型原本就是为了避免这种问题而出现的

表述性:不像带有类型实参的泛型即参数化类型那样,让人看到实参就知道里面的元素E都是什么类型。

泛型的子类型化的原则List<String>类型的原生类型是List的一个子类型,而不是参数化类型List<Object>的子类型。(泛型不可变性)

4. List、List<?>List<Object>的区别

  • List,即原始类型,其引用变量可以接受任何对应List<E>的参数化类型,包括List<?>,并且可以添加任意类型的元素。但其缺点在于不安全性、不便利性、不表述性(不应该使用原生类型的原因)。
  • List<?>,即通配符类型,其引用变量,同样可以接受任何对应List<E>的参数化类型,包括List,但不能添加任何元素,但可以removeclear,并非immutable(不可变)集合。List<?>一般作为参数来接收外部集合,或者返回一个具体元素类型的集合,也称为通配符集合。保证了安全性和表述性。但不具有表述性,从中取出的元素是Object类型,需要通过手动转换才能得到原本的类型
  • List<Object>,即实际类型参数为Object的参数化类型,其引用变量可以接受List,可以添加元素,但不能接受除了其本身外的任何参数化类型(泛型不可变性)。

    可以看到相比参数化类型的List<Object>List<?>缺点在于不能添加任何元素并且不具有便利性,如果这无法满足功能要求可以考虑使用泛型方法和有边界的通配符。

三、开发中遇到的问题

Object参数与List<Object>参数的方法调用哪一个?

 public boolean lSet(List<Object> value) {...}public boolean lSet(Object value) {...}

调用时

 List<Integer> list = new ArrayList<>();list.add(1);list.add(2);lSet(list);  //调用了lSet(Object value)

通过上面的原理,可以知道这种情况是由于泛型的不可变性导致的。要想要lSet(list)调用lSet(List)类型的方法,有以下几种方案。

  • 将传入的List<Integer> 转换为List,这样List<Object>可以接受;
  • 修改lSet(List)定义为以下几种之一即可
 // 参数传入之后,在方法内部不能对value进行添加元素的操作public boolean lSet(List<?> value) {...}// 或者List<? extends Object>,在方法内部都不能对value进行添加元素的操作,这种方案与上面的方案都属于采用【协变】的方式。public boolean lSet(List<? extends Integer> value) {...}// 采用【逆变】的方式声明,在方法内部能够对value进行写操作,但是进行读操作时,只能以根类Object进行读取,读取之后可以进行强转操作。public boolean lSet(List<? super Integr> value) {...}// 采用原型方式,不建议使用集合原型List等作为参数类型

V… values与Collection<V> values的区别

Java可变参数

定义方法时,在最后一个形参后加上三点...,就表示该形参可以接受多个参数值,多个参数值被当成数组传入。上述定义有几个要点需要注意:

  • 可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数
  • 由于可变参数必须是最后一个参数,所以一个函数最多只能有一个可变参数
  • Java的可变参数,会被编译器转型为一个数组
  • 变长参数在编译为字节码后,在方法签名中就是以数组形态出现的。这两个方法的签名是一致的,不能作为方法的重载。如果同时出现,则不能通过编译。可变参数可以兼容数组,反之不成立。
可能出现的问题

使用Object...作为变成参数:

public void foo(Object... args) {System.out.println(args.length);
}foo(new String[]{"arg1", "arg2", "arg3"}); //3
foo(100, new String[]{"arg1", "arg1"}); //2foo(new Integer[]{1, 2, 3}); //3
foo(100, new Integer[]{1, 2, 3}); //2
foo(1, 2, 3); //3
foo(new int[]{1, 2, 3}); //1

int[]无法转型为Object[],因而被当作一个单纯的数组对象;Integer[]可以转型为Object[],可以作为一个对象数组。

V… values的含义

通过变长参数的定义,V... values作为函数形参时,将会根据被虚拟机解析为V[] values。其中的泛型类型将会替换为实际的泛型参数类型

Collection<V> values的含义

它作为函数形参时,将会匹配实现了该接口的类型,比如ArrayList<V>

Java集合泛型作为参数时,使用中的一些问题。包括但不限于PECS原则相关推荐

  1. 002 Java集合泛型面试题

    Java集合/泛型面试题 1 ArrayList和linkedList的区别 ArrayList: 可以看作是能够自动增长容量的数组 ArrayList底层的实现是Array, 数组扩容实现 Arra ...

  2. JAVA集合泛型,类型擦除,类型通配符上限之类的知识点

    感觉定义要比PYTHON严谨很多,一切源于静态语言的特点吧.. 于是语法上就复杂很多,值不值得呢? 参考测试URL: http://www.cnblogs.com/lwbqqyumidi/p/3837 ...

  3. Java函数泛型List参数,操作泛型元素

    一个例子: public <E> void remove(List<E> list, E e) {Iterator iterator = list.iterator();whi ...

  4. Java面试题汇总2021最新(集合泛型含答案下载)

    Java面试题及答案2021最新24题(集合&泛型) 最近给大家整理了一批Java面试题一共24题,主要是搜集的Java集合&泛型这块的,是20201最新时间整理的,并且都含答案打包下 ...

  5. 第八章 Java集合

    Java集合类是一种特别有用的工具类,可用于存储数量不等的对象,并可以实现常用的数据结构,如栈.队列等.此处之外Java集合还可以用于保存具有映射关系的关联数组.Java集合大致可分为Set.List ...

  6. java 获取泛型_聊聊Java泛型擦除那些事

    >版权申明]非商业目的注明出处可自由转载 博文地址:https://blog.csdn.net/ShuSheng0007/article/details/89789849 出自:shushen ...

  7. java集合框架(Framework)的性能

    关于Java集合框架里面常用类的性能测试比较,包括(ArrayList/LinkedList /Vector/Queue/TreeSet/HashSet/LinkedHashSet/TreeMap/H ...

  8. java集合作为参数 传递的是_Java:数组和集合类作为参数传递时的差别

    最近在做项目时遇到一个List集合作为参数传递的问题,想起了以前总结的参数传递,参数传递包括值传递和引用传递,集合类的参数应该属于引用传递,脑子里突然就闪现到了数组,感觉数组也是一种特殊的集合,也应该 ...

  9. Java集合(八) 迭代器Iterator、泛型、Map映射

    一. 迭代器 迭代器的增删作用于原集合 用迭代器遍历集合 , 用指针的挪动来获取对应的元素,通过标记这个元素是否可以用来操作 去操作原集合. 在迭代过程中不允许直接操作原集合. forEach --- ...

最新文章

  1. 从中科院到BAT,如何准备秋招那件事儿(附B站录播)
  2. Variational Bayes
  3. Unity UI和引用的管理中心
  4. PHP点歌插件,斗鱼弹幕点歌插件_小葫芦社区_小葫芦插件交流 - Powered by Discuz!
  5. panel,dialog,window组件越界问题汇总
  6. Java中使用ProcessBuilder启动、管理应用程序
  7. 池流程图_干货收藏 | Java程序员必备的一些流程图
  8. AJAX跨域问题解决一:使用web代理
  9. Youki的装机日记~
  10. 【引用】mkswap 把一个分区格式化成为swap交换区
  11. 排列组合的思考、组合数的推广和拓展
  12. can卡、usbcan、can分析仪通用测试软件LCANTest详细介绍
  13. linux终端设置为管理员权限,ubuntu 中的管理员权限
  14. C#笔记——自动关机or定时关机小程序
  15. AJDK-Wisp协程
  16. Scrapy框架之Spiders类理解
  17. [iOS Xib加载/封装] xib加载以后无法赋值Laber属性的值
  18. JS Binding 技术(1)
  19. sync、fsync、fdatasync、fflush函数区别和使用举例
  20. nodejs实现ocr

热门文章

  1. 【备忘】win 10访问华为荣耀Pro 2路由器USB文件共享
  2. UA大全-user-agent大全
  3. python制作电子相册_电子相册 · 树莓派终极学习套件教程 · 看云
  4. Minitab 推出过程改善新的 Monte Carlo 仿真软件
  5. pygame教你从0到1一步步实现点到点的智能追踪系统(其二)
  6. NFS服务器的搭建(文件共享)
  7. weib后台管理中如何去除横向滚动框
  8. 数据仓库系统架构流程图
  9. 为什么总是天妒英才呢?因为没人管笨蛋活多久。
  10. 7.试定义RECT类(长方形)及其派生类CUB(长方体)