Skip to content

JavaGenerics

kcp edited this page Jul 13, 2020 · 1 revision

title: Java泛型 date: 2018-11-21 10:56:52 tags: - 泛型 categories: - Java

目录 start

  1. 泛型
    1. 简单使用
    2. 类型擦除
    3. 约束和局限性
    4. 泛型类型的继承规则
    5. 通配符类型
      1. 子类 类型限定的通配符 extends
      2. 基类 类型限定的通配符 super
      3. 无限定通配符
      4. 通配符捕获
    6. 反射和泛型

目录 end|2020-05-17 16:13|


泛型

Generics

泛型程序设计划分为三个熟练级别 基本级别就是仅仅使用泛型类,典型的是像ArrayList这样的集合--不必考虑他们的工作方式和原因,大多数人会停留在这个级别.直到出现了什么问题. 当把不同的泛型类混合在一起的时候,或是对类型参数一无所知的遗留代码进行对接时,可能会看到含糊不清的错误消息.如果这样的话,就需要系统的进行学习Java泛型来系统地解决问题.
泛型类可以看作普通类的工厂 -- Java核心技术卷 2004(1.5)


开始学习的兴趣来源 Java帝国之泛型

参考: Java总结篇系列:Java泛型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
参考: Java深度历险(五)——Java泛型

简单使用

例如该行定义 : public abstract class RoomCache<P extends PlayerBO, M extends MemberBO, V extends VideoDataBO<M>, R extends RoomBO<M, V>> extends AbstractCache<PlatformRoomId, R> {}

  • 类型变量使用大写的一个字母这是代表:
    • E 集合的元素类型
    • K V 表示表的关键字和值的类型
    • T U S 等就表示任意类型
    // 根据Class对象 获取泛型的类型信息
    Type superClass = getClass().getGenericSuperclass();
    type = ((ParameterizedType) superClass).getActualTypeArguments()[1];

参考: 使用通配符简化泛型使用

  • 场景1:public static <T extends Comparable<T>> T min(T[] list);

    • 限定了入参和返回值是 是实现了Comparable接口的某个类型 因为Comparable也是一个泛型类, 所以也进行限定类型
    • 这样的写法要比 T extends Comparable 更为彻底
    • 例如计算一个String数组的最小值 T 就是 String类型的, String是Comparable的子类型
      • 但是当处理GregorianCalendar, GregorianCalendar是Calendar的子类, 并且Calendar实现了Comparable<Calendar>
      • 因此GregorianCalendar实现的是Comparable<Calendar>, 而不是Comparable
      • 这种情况下 public static <T extends Comparable<? super T>> T min(T[] list) 就是安全的
  • 场景2: public static <T extends ExcelTransform> List<T> importExcel(Class<T> target)

    • 该方法实现了, 传入继承了ExcelTransform接口的类对象, 得到该类的List集合
    • <T extends ExcelTransform> boolean 这样写编译没报错, 那么就是说, 就是一个泛型的定义, 后面进行引用, 省的重复写
    • 简单的写法就是 public static <T> List<T> importExcel(Class<T> target)
  • 场景3: Spring4.x 添加的泛型依赖注入 , 使用的JPA就是依赖该技术 spring学习笔记(14)——泛型依赖注入

  • 场景4: 泛型嵌套以及传递问题 实际代码

    • 本来的设想是只要声明了具有泛型约束的类, 就应该不用再声明该类中的泛型类型, 但是由于Java的泛型只是在编译前存在, 编译后就被擦除了, 所以没法做到这样简洁的约束

对于应用程序员, 可能很快的学会掩盖这些声明, 想当然地认为库程序员做的都是正确的, 如果是一名库程序员, 一定要习惯于通配符
否则还要用户在代码中随意地添加强制类型转换直至可以通过编译.


类型擦除

  • 不同于C++的泛型,C++是将模板类组合出来生成一个新的类,Java则是进行类型擦除,然后再类型强转

  • 例如 public static <T extends Comparable> T min (T[] list)

    • 擦除后 public static Comparable min(Comparable[] list)
  • 泛型类擦除示例

  • 例如该方法签名 public static <T extends Comparable & Serializable> T getMax(T[]list)

    • 限制了必须是实现了两个接口的类才能使用, 估计为了少创关键字所以使用的是extends关键字来表示T要实现两个接口
    • 同样的可以加在类的签名上,进行限制类的泛型类型 public class Pair <T extends Comparable>{}

在Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多只有一个类,如果用一个类作为限定,他必须是限定列表中的第一个

注意:泛型记录在类字节码中的 Signature LocalVariableTypeTable 属性上, 参考: Java泛型-4(类型擦除后如何获取泛型参数)


约束和局限性

以下代码示例:涉及的类Pair在上述的代码中已经定义, Human和Student是继承关系

  • 不能使用基本类型 实例化类型参数

    • 也就是说没有 Pair<double> 只有 Pair<Double>
    • 因为类型擦除后,类型是Object并不能放double的值, 但是这样做与Java语言中基本类型的独立状态相一致.
    • 但是 可以使用 原始类型数组 例如 byte[]
    • valhalla项目正计划支持原始类型
  • 运行时类型查询(eq或者instanceof)只适用于原始类型

    • 比如 Pair<T>Pair<String> 是等价的,因为类型擦除
    • Pair<String> pair1 和 Pair<Date> pair2 pair1.getClass() 和 pair2.getClass() 是等价的都是返回Pair.class
  • 不能抛出也不能捕获泛型类实例

    • 错误的示例:
      • public class Problem<T> extends Exception{}
      • public static <T extends Throwable> void doWork(){try{}catch(T t){}}
    • 正确示例:
      • 在异常声明中使用类型变量
      • public static <T extends Throwable> void doWork() throws T{.. catch(){throw t;}}
  • 参数化类型的数组不合法

    • 例:Pair<String>[] list = new Pair<String>[10];
    • 因为擦除后 list是 Pair[] 类型, 能转成 Object[] 这样就失去了泛型的作用
    • 如果要使用的话最好直接使用集合 ArrayList: ArrayList<Pair<String>> 安全又高效
        Object[] array = list;
        array[0] = "hi";//  编译错误
        array[0] = new Pair<Date>(); //通过数组存储的检测,但实际上类型错误了,所以禁止使用参数化类型的数组
  • 不能实例化类型变量(T)以及数组

    • 非法 new T(){}
    public Pair(){
        first = new T();
        second = new T();
    }
    
    //非法 T.class是不合法的
    first = T.class.newInstance() 

    //要实例化一个Pair<T>的对象就要如下:
    public static <T> Pair<T> initPair(Class<T> c){
        try{
            return new Pair<T>(c.newInstance(), c.newInstance());
        }catch (Exception e){
            return null;
        }
    }
    // 如下调用
    Pair<String> pair = Pair.initPair(String.class);
    // 因为Class本身是泛型, String.class其实是Class<String>的实例
    // 也不能实例化为一个数组 new T[5]
  • 泛型类的静态上下文中类型变量无效
    • 不能在静态域中使用类型变量 如下:
    • 如果这段代码能执行,那就可以声明一个 Singleton 共享随机数生成类,
    • 但是声明之后,类型擦除,就只剩下了Singleton类,并不能做对应的事情,所以禁止这样的写法
    private static T first; // 错误
    public static T getFirst(){ // 错误
        return first;
    }
  • 注意泛型擦除后的冲突
    • 当类型擦除时,不能创建引发冲突的相关条件
    • 例如 新实现一个类型变量约束的equals方法就会和Object原方法冲突 补救方法就是重命名该方法了
    public class Pair<T>{
        public boolean equals (T value){
            return ..
        }
    }

泛型规范说明

  • 要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化
    • 以下代码就是非法的, GregorianCalendar 实现了两个接口,两个接口是Comparable接口的不同参数化,这是不允许的
    class Calendar implements Comparable<Calendar>{}
    class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{} // 错误
  • 但是如下又是合法的
    class Calendar implements Comparable{}
    class GregorianCalendar extends Calendar implements Comparable{}
  • 很有可能是桥方法有关,不可能有两个一样的桥方法(因为两个接口其实是一个接口的不同参数化,桥方法的方法签名是一致的)

泛型类型的继承规则

例如 父子类: Human Student 那么 Pair Pair 是继承(inherit)关系么,答案是否定的!!

    Pair<Human> humans = new Pair<Human>(man, woman);
    Pair<Student> classmates = humans;// illegal, but suppose it wasn't

    classmates.setSecond(junior) // 如果上面合法,那么这里是肯定可以执行的, 因为泛型类型变成了Student
    //那么就有了问题了,原有的人类类型限制的对象中,出现了小学生
    //所以不允许这样的类型变量约束的类进行多态
    
    // 但是数组可以这样写是因为数组会有自己的检查保护
    Human[] humans = {man, woman};
    Student[] students = humans;
    students[0] = junior ;// 虚拟机将抛出 ArrayStoreException 异常

为何 List<Object> list = Arrays.asList("1","2"); 能通过编译


永远可以将参数化类型转换为一个原始类型, Pair 是原始类型Pair的一个子类型,转换成原始类型也会产生错误
相关测试类

    Pair<Human> humans = new Pair<Human>(man, woman);
    Pair other = humans;
    other.setFirst(new String("wtf"))// 只是会有一个编译时的警告(类型未检查),但实际上都看得出这明显是错误的
    // 那么在后续代码中继续当做Human对象进行引用,必然就会有ClassCastException
    // 所以这样的写法尽量避免,这里的设计 就失去了泛型程序设计提供的附加安全性.(挖的坑)

泛型类可以扩展或实现其他的泛型类,就这一点而言,和普通类没有什么区别

  • 例如 ArrayList 实现List接口, 这意味着一个ArrayList可以转换为List
    • 但是一个ArrayList不是ArrayList或者List.

通配符类型

Guidelines for Wildcard Use

  • <T> 可以看作 <T extends Object> <?> 可以看作 <? extends Object>

  • 协变(covariant)和逆变 (contravariant)

    • 协变 是指能够使用与原始指定的派生类型相比,派生程度更大的类型。
      • 例如 String -> Object
    • 逆变 是指能够使用派生程度更小的类型。
      • 例如 Object -> String
  • Producer extends, Consumer super.

    • ? extends : 数据的提供方 执行 get 操作
    • ? super : 数据的存储方 执行 set 操作
  • Tips

    • 限定通配符总是包括自己
    • 如果你既想存,又想取,那就别用通配符
    • 不能同时声明泛型通配符上界和下界

注意 通配符的泛型约束一般是出现在基础库的API上(接口上, 方法上) 常见应用逻辑代码用的较少

子类 类型限定的通配符 extends

通配符上限 顾名思义,就是限定为该类及其子类

  • 例如:
    • Pair<? extends Human> 表示任何Pair泛型类型并且他的类型变量要为Human的子类
    • 编写一个方法 public static void printMessage(Pair<Human> human){}

正如上面所说, Pair类型的变量是不能放入这个方法的,因为泛型变量是没有继承关系, 这时候就可以使用这个通配符:

public static void printMessage(Pair<? extends Human>) 可以get不能set

    Pair<Human> humans = new Pair<Human>(man, woman);
    Pair<? extends Human> classmates = humans;// 编译通过
    classmates.setSecond(junior) // 编译错误,泛型约束起作用了

    // 分析其泛型类实现可以理解为:
    ? extends Human getFirst()
    void setFirst(? extends Human)
    // 这样的话是不可能调用setFirst方法, 对于编译器来说,只是知道入参是Human的子类,但是类型并不明确,所以不能正常调用
    // 使用get方法就不会有问题, 泛型起作用了.将get返回值赋值给Human的引用也是完全合法的,这就是引入该统通配符的关键之处

基类 类型限定的通配符 super

通配符下限 顾名思义就是限定为父类, 通配符限定和类型变量限定十分相似, 但是可以指定一个超类型限定(supertype bound)
? super Student 这个通配符就限定为Student的所有超类型(super关键字已经十分准确的描述了这种关系)

带有超类型限定的通配符的行为和前者相反,可以为方法提供参数,但不能使用返回值即 可以 set 但是不能get

    // Pair<? super Student> 例如这种定义
    void setFirst(? super Student)
    ? super Student getFirst()
    // 编译器不知道setFirst方法的确切类型,但是可以用任意Student对象(或子类型) 调用他, 而不能使用Human对象调用.
    // 然而,如果调用getFirst,泛型没有起作用,只能将返回值用Object接收

以上两种情况的相关测试类

总结: 类定义上的泛型变量:

子类型限定: <? extends Human> 是限定了不能set,但是保证了get
超类型限定: <? super Student> 限定了不能正确get,但是保证了set.

无限定通配符

Unbounded Wildcards

    // 例如 Pair<?>
    ? getFirst() // 方法的返回值只能赋值给一个Object
    void setFirst(?) // 方法不能被调用,甚至不能用Object调用.
    // Pair<?> 和 Pair 本质的不同在于: 可以用任意Object对象调用原始的Pair类的setObject(set方法,因为类型擦除 入参是Object, 简称setObject)方法 
  • 例如 这个hasNull()方法用来测试一个pair是否包含了指定的对象, 他不需要实际的类型.

通配符捕获

Wildcard Capture and Helper Methods

  • 如果编写一个交换的方法
    public static void swap (Pair<?> p){
        ? temp = p.getFirst(); // 错误, 不允许将?作为类型
        p.setFirst(p.getSecond());
        p.setSecond(temp);
    }
  • 但是可以编写一个辅助方法
    public static <T> void swapHelper(Pair<T> p){
        T temp = p.getFirst();
        p.setFirst(p.getSecond());
        p.setSecond(temp);
    }
  • swapHelper是一个泛型方法, 而swap不是, 它具有固定的Pair<?>类型的参数, 那么现在就可以这样写:
    • public static void swap(Pair<?> p){swapHelper(p);}
    • 这种情况下, swapHelper方法的参数T捕获通配符, 它不知道是哪种类型的通配符,但是这是一个明确的类型 并且swapHelper 在T指出类型时,才有明确的含义
    • 当然,这种情况下并不是一定要用通配符, 而且我们也实现了没有通配符的泛型方法

但是下面这个通配符类型出现在计算结果中间的示例

    public static void maxMinBonus(Student[] students, Pair<? super Student> result){
        minMaxBonus(students, result);
        swapHelper(result);
    }
    // 在这里,通配符捕获机制是不可避免的, 但是这种捕获只有在许多限制情况下才是合法的.
    // 对于编译器而言, 必须能够确信通配符表达的是单个, 确定的类型.

反射和泛型

Official Doc: Class

JDK中Class类也泛型化了, 例如String.class实际上是Class<String>类的对象(事实上是唯一的对象)
类型参数十分有用, 这是因为他允许Class<T>方法的返回类型更加具有针对性.

Class<T>的方法就使用了类型参数

    T newInstance()
    T cast(Object obj)
    T[] getEnumConstants()
    Class<? super T> getSuperclass()
    Constructor<T> getConstructor(Class... paramterTypes)
    Constructor<T> getDeclaredConstructor(Class... paramterTypes)
  • newInstance方法返回一个示例, 这个实例所属的类由默认的构造器获得, 它的返回类型目前被声明为T, 其类型与Class<T>描述的类相同, 这样就免除了类型转换.
  • 如果给定的类型确实是T的一个子类型, cast方法就会返回一个现在声明为类型T的对象, 否则, 抛出一个BadCastException异常
  • 如果这个类不是enum类或类型T的枚举值的数组, getEnumConstants方法将返回Null.
  • getConstructorgetDeclaredConstructor方法返回一个Constructor<T>对象.Constructor类也加上了泛型, 方便newInstance方法有正确返回类型.

TODO 还要继续看书

    // 传入一个Class对象, 得到Class对应类型的实例
    public <T> T get(Class<T> target);
    // 类型加上约束
    public <T extends Runable> T get(Class<T> target);

Summary

Clone this wiki locally