深入理解Java 泛型

200 阅读7分钟

基本概述:

  • 本文以图示 + 代码 + 文字的形式,向读者介绍了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 检查?
    • 可以的,因为数组类型默认开启了协变,因此,可以使用父类引用接收子类对象 image.png
  • 问题二:为什么运行时会崩溃?
    • 不可以,这是数组类型默认开启协变而带来的后遗症,编译期是没有问题的,但运行时,类型对不上
      • 崩溃示意:因为str2 本质是String 数组而非Integer image.png
      • 异常问题:如果说数组对象默认禁止协变就不会出现这个问题
        image.png

开篇导入

  • 概要图: image.png
  • Demo 中的继承关系: image.png
  • 代码展示:
    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 二者都支持 image.png

协变相关:

什么是协变:

  • 父类引用接收子类对象

JDK 默认禁止使用处泛型:针对于“不变”

  • 不允许:List<父类> = List<子类>
  • 代码示例A: image.png
  • 代码示例B:
    image.png

为什么不允许使用处泛型开启协变?

  • 会存在类型转换异常:存进去的是Student 类型,取出来的是SubStudent image.png

如何开启使用处泛型:? + extends

  • 允许:List<? extends 父类> = List<子类>
  • 代码展示A : image.png
  • 代码示例B: image.png
  • 补充说明:源码展示:
    • 在Android 大量源码采用了此种设计:可以接收Object 及其所有子类 image.png

思考题

  • 问题:若代码放开是否报错?

    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 的所有子类>
      • 验证:通过编译期检查 image.png

      • 逆变相关:子接收父

        什么是逆变:

        • 子类引用接收父类对象

        JDK 内部默认禁止逆变:针对于“不变”

        • 代码示例A: image.png
        • 代码示例B: image.png

        如何手动开启逆变:? + super

        • 代码示例A: image.png
        • 代码示例B: image.png

        Kotlin 部分

        Kotlin 内的协变

        • 异常使用协变:接受自己的父类 + 不在继承关系的类 image.png
        • 正常使用协变:接收自己 + 自己的子类 image.png

        Kotlin 内的逆变

        • 正常使用逆变:接收自己 + 父类及以上 image.png
        • 异常使用逆变:接受子类及以下 + 非继承关系的类 image.png

        生产者 - 消费者模型

        基本概述:

        • 如果我们只需要读取泛型数据(我们是生产者,读取数据准备给别人用),就可以将此泛型声明为协变
          • Java:? extends T;
          • Kotlin:out T
        • 如果我们只需要写入泛型数据(我们是消费者,将数据进行消费),就可以将此泛型声明为逆变
          • Java:? super T
          • Kotlin:in T

        生产者模式:

        • kotlin 版本:只读模式 image.png
        • 源码中的例子:ArrayLIst 的构造方法
          • 使用协变机制:仅在此类中存数据时,保证数据安全,但是ArrayList 这个数据结构不是线程安全的
            • 对于下面的 c 是无法修改的

              image.png

        消费者模式

        • Kotlin 版本:如果我们只需要写入泛型数据(我们是消费者,将数据进行消费),就可以将此泛型声明为逆变 image.png
        • 源码中的例子:
          • ForEach 只需要消费掉传入的数据,但是在这个类中,forEach() 外,是没有读取权限的 image.png

        生产者 - 消费者在源码中的例子

        • 自定义生产者 - 消费者模式 image.png
        • 源码中的例子:在Collections 的copy()
          • 蓝色部分使用协变,为消费者,只读取数据
          • 红色部分使用逆变,为生产者,仅消费数据 image.png

        实际使用案例

        只读模式:

        • 代码示例: image.png

        只能消费:

        • 代码示例 image.png

        • 其实还是可以读取的:声明为Object 即可

          for (Object animal : List){
              
          }
          

        源码探究

        • 协变带来的后遗症:协变为只读模式,在修改时将出现问题

          • 查看Collection 的addAll():使用了协变

            image.png

          • 查看Collections.removaAll():没有使用泛型了,而是采用的Object

            image.png

        泛型擦除相关

        泛型擦除的意义

        • 对于低版本:擦了就擦了,无所谓,因为本来就是Object
        • 对于高版本:需要对泛型信息做保存记录,实际上在取出时,是做了强转的 image.png

        早期JDK:1.5 之前没有泛型的时候

        • 不支持泛型,全部使用Object 来处理的
          • 代码示例:C# 为真泛型,Java 为假泛型
            public interface GenericBefore1.5{
                Object getInfo();
                
                void setInfo(Object info);
            }
            

        在JDK 后期引入了泛型

        • 但是为了兼容之前的代码,在编译完之后,需要将泛型 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,就会有问题,因为擦除的时候,是无法知道我有无参数的 image.png

        泛型擦除带来的问题

        • 代码示例B:被擦除后永远为真 image.png
        • 代码示例C:当泛型被擦除后,int 不是Integer 的子类,擦都擦不了 image.png

        问题解答:

        • GSON 为什么知道去序列化那个类

          • GSON 去拿泛型的signature 表,里面保存了泛型信息

        补充:PECS 原则

        • 早期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进行兜底读取。