基本概述:
-
本文以图示 + 代码 + 文字的形式,向读者介绍了Java 语言中的协变,逆变,不变、生产者、消费者、只读模式、修改模式、泛型擦除的原理
-
精华凝练:
- 当明确写入什么、读取什么,使用不变
- 当明确写入什么,但不希望被任何地方读取敏感数据,仅仅消费此数据,使用逆变( ? super、in、消费者)
- 当明确读取什么,但不希望数据有任何被修改的可能,仅仅读取此数据,使用协变(? extends、out、生产者)
前情概览
- 代码示例:
public class Main { public static void main(String[] args) { String[] str1 = new String[2]; Object[] str2 = str1;//父类引用接收子类对象 str1[0] = "WAsbry"; str2[1] = Integer.valueOf(767);//为什么会崩溃 } }
- 问题一:下面这行代码能否通过IDEA 检查?
- 可以的,因为数组类型默认开启了协变,因此,可以使用父类引用接收子类对象
- 可以的,因为数组类型默认开启了协变,因此,可以使用父类引用接收子类对象
- 问题二:为什么运行时会崩溃?
- 不可以,这是数组类型默认开启协变而带来的后遗症,编译期是没有问题的,但运行时,类型对不上
- 崩溃示意:因为str2 本质是String 数组而非Integer
- 异常问题:如果说数组对象默认禁止协变就不会出现这个问题
- 崩溃示意:因为str2 本质是String 数组而非Integer
- 不可以,这是数组类型默认开启协变而带来的后遗症,编译期是没有问题的,但运行时,类型对不上
开篇导入
- 概要图:
- Demo 中的继承关系:
- 代码展示:
public class Simple1 { public void static main(String[] args){ Person person = new SubStudent;//对象继承关系,默认开启协变 } }
基本概念:
- 变形有哪些:分为不变(普通泛型)、协变(? + extends)、逆变(? + super)
- 声明处泛型:声明类或者接口时涉及泛型
interface Collection<E>{//声明处泛型 void addAll(Collection<E> items) } - 使用处泛型:定义类或接口时,涉及泛型
List<Person> personList = new ArrayList<>();//使用处泛型 - Java 不允许声明处泛型,仅允许使用处泛型;但Kotlin 二者都支持
协变相关:
什么是协变:
- 父类引用接收子类对象
JDK 默认禁止使用处泛型:针对于“不变”
- 不允许:
List<父类> = List<子类> - 代码示例A:
- 代码示例B:
为什么不允许使用处泛型开启协变?
- 会存在类型转换异常:存进去的是Student 类型,取出来的是SubStudent
如何开启使用处泛型:? + extends
- 允许:
List<? extends 父类> = List<子类> - 代码展示A :
- 代码示例B:
- 补充说明:源码展示:
- 在Android 大量源码采用了此种设计:可以接收Object 及其所有子类
- 在Android 大量源码采用了此种设计:可以接收Object 及其所有子类
思考题
-
问题:若代码放开是否报错?
public class Main { interface MyCollections<E>{ void addAll(MyCollections<? extends E> collection); } void copyAll(MyCollections<Object> to,MyCollections<String> from){ // to.addAll(from);此行代码放开是否会报错 } public static void main(String[] args) { } } -
分析思路:
- 核心在于检验参数的合法性,
- 也就是检验 to = from 是否成立,
- 进一步讲MyCollection 的引用是否可以接收MyCollction 的对象,
- 也就是将MyColletion 是否为MyCollection 的父类
- 所以代码放开不会报错,因为依照前文所述,MyCollection 可接收MyCollection<Object/Object 的所有子类>
-
验证:通过编译期检查
- 子类引用接收父类对象
- 代码示例A:
- 代码示例B:
- 代码示例A:
- 代码示例B:
- 异常使用协变:接受自己的父类 + 不在继承关系的类
- 正常使用协变:接收自己 + 自己的子类
- 正常使用逆变:接收自己 + 父类及以上
- 异常使用逆变:接受子类及以下 + 非继承关系的类
- 如果我们只需要读取泛型数据(我们是生产者,读取数据准备给别人用),就可以将此泛型声明为协变
- Java:? extends T;
- Kotlin:out T
- 如果我们只需要写入泛型数据(我们是消费者,将数据进行消费),就可以将此泛型声明为逆变
- Java:? super T
- Kotlin:in T
- kotlin 版本:只读模式
- 源码中的例子:ArrayLIst 的构造方法
- 使用协变机制:仅在此类中存数据时,保证数据安全,但是ArrayList 这个数据结构不是线程安全的
-
对于下面的
c是无法修改的
-
- 使用协变机制:仅在此类中存数据时,保证数据安全,但是ArrayList 这个数据结构不是线程安全的
- Kotlin 版本:如果我们只需要写入泛型数据(我们是消费者,将数据进行消费),就可以将此泛型声明为逆变
- 源码中的例子:
- ForEach 只需要消费掉传入的数据,但是在这个类中,forEach() 外,是没有读取权限的
- ForEach 只需要消费掉传入的数据,但是在这个类中,forEach() 外,是没有读取权限的
- 自定义生产者 - 消费者模式
- 源码中的例子:在Collections 的copy()
- 蓝色部分使用协变,为消费者,只读取数据
- 红色部分使用逆变,为生产者,仅消费数据
- 代码示例:
-
代码示例
-
其实还是可以读取的:声明为Object 即可
for (Object animal : List){ } -
协变带来的后遗症:协变为只读模式,在修改时将出现问题
-
查看Collection 的addAll():使用了协变
-
查看Collections.removaAll():没有使用泛型了,而是采用的Object
-
- 对于低版本:擦了就擦了,无所谓,因为本来就是Object
- 对于高版本:需要对泛型信息做保存记录,实际上在取出时,是做了强转的
- 不支持泛型,全部使用Object 来处理的
- 代码示例:C# 为真泛型,Java 为假泛型
public interface GenericBefore1.5{ Object getInfo(); void setInfo(Object info); }
- 代码示例:C# 为真泛型,Java 为假泛型
- 但是为了兼容之前的代码,在编译完之后,需要将泛型 T 擦除,变为Object
public interface GenericBefore1.5<T>{ T getInfo(); void setInfo(Object info); } - 泛型擦除是JDK 早期设计的一大缺陷,泛型擦除是补救措施
- 擦除前
public interface MyInterface<T extends Animal>{ T getInfo(); void setInfo(T info); } - 擦除后:所以,为什么协变是不能传父类及以上的
public interface MyInterface{ Animal getInfo(); void setInfo(Animal info); } - 代码示例A:泛型使用处报错
- 假如,我传入的是Student 在擦除后,是没有问题的
- 但是,我传入的是Person,就会有问题,因为擦除的时候,是无法知道我有无参数的
- 代码示例B:被擦除后永远为真
- 代码示例C:当泛型被擦除后,int 不是Integer 的子类,擦都擦不了
-
GSON 为什么知道去序列化那个类
- GSON 去拿泛型的signature 表,里面保存了泛型信息
-
早期JDK 设计中对于数组类型是不支持协变的,导致会重载许多方法
-
简述PECS原则
- PECS原则全称"Producer Extends, Consumer Super",即上界生产,下界消费。
-
如果有一个List
List list = new ArrayList(); -
如果在一个情景下限制只能从里面取数据(生产者),那么可以对外暴露如下引用:
List<? extends People> list1 = list;- 原理:<? extends People>限制了该引用的实际泛型一定是People的某个子类,即list1引用实际指向的ArrayList中只能存储People的子类(可以向上转型成People),但由于是“某个子类”,所以不能确定其类型,那么在进行存储时也就无法确定需要向上转型的泛型。
- 但是在读取时,因为实际的list中存储的一定是People的子类以及该子类的子类,那么一定可以用People引用进行读取(向上转型);
-
可能比较绕的点,主要是转型方向的问题:存的时候要将存的数据向上转型成list中的类型,取的时候要将list中的类型向上转型成取出的引用类型。
- 相反,如果想限制只能向list里面写数据(消费者),那么可以对外暴露如下引用:
List<? super Men> list2 = list;
- 原理:List<? super Men>限制了该引用的实际泛型一定是Men的某个父类,即list2引用实际指向的ArrayList中只能存储Men的父类,那么在存储时,只需要使用Men类型的引用,那么一定可以向上转型成list的实际泛型。 但是在读取时,由于无法确定list的实际泛型,可能是Men的任意父类,只能用终极父类Object进行兜底读取。
- 相反,如果想限制只能向list里面写数据(消费者),那么可以对外暴露如下引用:
逆变相关:子接收父
什么是逆变:
JDK 内部默认禁止逆变:针对于“不变”
如何手动开启逆变:? + super
Kotlin 部分
Kotlin 内的协变
Kotlin 内的逆变
生产者 - 消费者模型
基本概述:
生产者模式:
消费者模式
生产者 - 消费者在源码中的例子
实际使用案例
只读模式:
只能消费:
源码探究
泛型擦除相关
泛型擦除的意义
早期JDK:1.5 之前没有泛型的时候
在JDK 后期引入了泛型
泛型擦除示例
泛型擦除带来的问题
泛型擦除带来的问题
问题解答:
补充:PECS 原则