JDK1.5引入的泛型语法特性,而为了与先前版本(没有泛型的版本)兼容,Java采用了一种类型擦除的方式,将编译后的字节码向前兼容;

概念

泛型的本质是为了 类型参数化,能避免创建新的类型,支持多种数据类型执行相同的代码(代码复用)

泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会进行类型检查)

若指定泛型的类型为A,那么所有使用到泛型的地方都会被指定为A,这些过程不需要强制类型转换;

不需要强制类型转换时什么意思?举个例子:List<Integer> list

在取元素的时候不用 Integer a = (Integer) list.get(0) 这种写法,而是直接 Integer a = list.get(0)

JDK1.5前

当没有泛型,是如何处理多个类型的呢?

要么针对每个类型都写一个类;

// 针对 Integer 的类
class BoxInteger {
    Integer value;
    void setValue(Integer value) { this.value = value; }
	Integer getValue() { return this.value; }
}

// 针对 String 的类
class BoxString {
    String value;
    void setValue(String value) { this.value = value; }
	String getValue() { return this.value; }
}

这种写法需要实现多个类,在代码维护时需要改动多个地方;

要么使用 Object 或者共同父类来写一个类,在使用时进行类型强转

class BoxCommon {

    private Object value;

    void setValue(Object value) { this.value = value; }
    
    Object getValue() { return this.value; }

    public static void main(String[] args) {
		// 针对 String 的
        BoxCommon stringBox = new BoxCommon();
        stringBox.setValue("123");
        String value = (String) stringBox.getValue();
        
		// 针对 Integer 的
        BoxCommon intBox = new BoxCommon();
        intBox.setValue(1234);
        Integer intValue = (Integer) intBox.getValue();
    }
}

这种写法虽然不需要多个类,但是需要类型强转,如果当类型不匹配时,会出现 ClassCastException 异常;

对于类型的转换需要额外的小心;

而泛型的引进,就是为了消除 ClassCastException

使用

<> 中声明泛型的名称,通常使用一个大写字母表示;

  • 允许同时声明多个泛型,多个泛型之间使用 , 区分:

  • 允许使用 extends 指定泛型的上界(不支持指定下界)

  • 允许使用 & 同时指定泛型需要实现的接口(支持同时指定多个)

泛型类

单个泛型:

class Group<T> {
    T[] members;
}

Group<Person> group = new Group<>();
group.members = new Person[10];

多个泛型:

// 多个泛型
class Pair<K, V> {
    K key;
    V value;
}

Pair<Integer, Double> pair = new Pair<>();
pair.key = 1;
pair.value = 2d;

泛型上界:

class Box<T extends Number> {
    T value;

    public static void main(String[] args) {
        Box<Integer> intBox =new Box<>();
        Box<String> stringBox = new Box<String>();
    }
}

& 声明泛型实现的接口:

class Box<T extends Number & Comparable & Serializable> {
     T value;
}

这个例子指 泛型T需要是Number的子类,同时需要实现 Comaparable 和 Serializanl 接口

静态方法和静态变量不能使用泛型声明的参数

  • 泛型是在定义对象时指定的:new Calculation<Integer>()

  • 而静态方法和静态变量不需要使用对象来调用,无法确定泛型的具体类型;

class Calculation<T extends Number> {
	// 编译错误
    static double add(T a, T b) {
        return a.doubleValue() + b.doubleValue();
    }
}

泛型接口

interface Usable<T> {
    
    void use(T obj);
    
}

class Mirror implements Usable<Mirror> {
    @Override
    public void use(Mirror obj) {
        System.out.println("照镜子");
    }
}

泛型方法

在调用方法时指定类型,与类是否指定泛型没有关系;

class Calculation {
	// 泛型上界
    static <T extends Number> double add(T a, T b) {
        return a.doubleValue() + b.doubleValue();
    }

    public static void main(String[] args) {
        System.out.println(add(10, 100f));
    }
}

型变

型变(variance)是指在使用泛型时,不同类型之间如何相互兼

主要分为:

  • 协变(covariance)

  • 逆变(contravariance)

  • 不变(invariance)

不变invariance

不变 表示泛型类型之间不能相互赋值;

也就是说,泛型类型不同,即使存在继承关系, 也会视作为不同的类型。

List<Number> numberList = new ArrayList<>(); 
List<Integer> intList = new ArrayList<>();
numberList = intList; // 编译错误

要取消这种限制,可以使用泛型通配符 <?> 表示接收任何类型的泛型:

List<String> list = new ArrayList<>();
List<?> allList = new ArrayList<>();
allList = list; // 正确

协变covariance

协变 表示可以将一个泛型类型的 子类型 对象赋值给 父类型 对象(看作是继承关系,泛型子类可以看做泛型父类的子类)

使用 <? extends T> 表示

List<? extends Number> list = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
list = intList; // 正确
intList = list; // 编译错误

逆变invariance

逆变 表示可以将一个泛型类型的 父类型 对象赋值给 子类型 对象(看作是反过来 "逆" 的继承关系,泛型父类可以看做泛型子类的子类)

使用 <? super T> 表示

List<? super Integer> intList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
intList = numberList; // 正确
numberList = intList; // 编译错误

作用

协变 是对子类型的约束宽松,让 子类型 的泛型能够赋值给 父类型 的泛型

代价是 只能取不能存,也就是只能作为 生产者(不能消费)

static void add(List<? extends Number> list) {
    list.add(1); // 错误!
}

如果外部传递的是一个 List<Double> 类型的List,若 add 函数中只要是继承了 Number 的对象都能添加,就会导致 List 中数据类型不一致,在使用时就会出现 ClassCastException

协变 是对父类型的约束宽松,让 子类型 的泛型能够赋值给 父类型 的泛型

代价是 只能存不能取,也就是只能作为 消费者(不能生产)

static void add(List<? super Integer> list) {
    Integer a = list.get(0); // 无法确定list的元素就是 Integer
}

如果外部传递的是 List<Number> 类型的 List,如果拿出来当 Integer 使用,也会出现 ClassCastException

但是允许添加元素,因为 Integer 允许赋值给 Number 类型的引用;


类型擦除

伪泛型

泛型特性是在 JDK1.5 引入的,为了兼容先前的版本,Java使用了 伪泛型

也就是在语法上可以使用泛型,但是编译的时候会进行 类型擦除(Type Erasure):

  • 将泛型替换成具体的类型,就好像没有泛型一样;

  • 这个替换并不是替换成指定的类型,而是根据上下界推导出的类型(指定的类型被擦除了)

可以测试下:

class A { }
class B { }

class Box<T> {
    T value;

    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>();
        Box<Number> numberBox = new Box<>();
        A a = new A();
        B b = new B();
        System.out.println(a.getClass() == b.getClass()); 
		// 输出 false
        System.out.println(intBox.getClass() == numberBox.getClass());
		// 输出 true
    }
}

不同的类 A 和 B 的 Class 对象不同,输出 false 正常;

直觉上 Box<Integer> 和 Box<Number> 是不同的类,其 Class 对象应该是不同的,但是上面输出的 true,说明 Box 的类型被擦除了,擦除后的类型都是 Box;


类型擦除原则

  • 删除泛型参数声明,也就是删除掉 <> 及其内部内容;

  • 根据泛型上下界推导出所需要替换的类型

    • 如果是无限制通配符 <?> 或者没有上下界限定 <T> ,那么替换为 Object

    • 如果指定了上界 <? extend A> 则会替换为 A;

    • 如果指定了下界 <? super A> 则会替换为 Object(取父类,A的父类不确定只能取 Object)

  • 确保类型安全,必要时插入强制类型转换代码;

  • 自动产生 “桥接方法” 以保证类型擦除后的代码仍然具有泛型的 “多态性”

对于无上下界的泛型,类型擦除:

  • <T><?> 无上下界的类型推断为 Object,并进行替换

class Box<T> {
    T value;

    Box(T initial) {
        value = initial;
    }
}

// ------ 类型擦除后 ----------------

class Box {
    Object value;

    Box(Object initial) {
        value = initial;
    }
}

对于有上下界的泛型,类型擦除:

  • 将类似 <T extends Number><? extends Number> 推断为 Number,进行替换;

  • 将类似 <T super Number> 推断为 Object,进行替换;

class Box<T extends Number> {
    T value;

    Box(T initial) {
        value = initial;
    }
}

// ------ 类型擦除后 ----------------

class Box {
    Number value;

    Box(Number initial) {
        value = initial;
    }
}

编译期检查

Java的编译器先通过检查代码中泛型的类型,通过类型检查后,才进行类型擦除,最后才编译;

下面这个例子,list类型擦除后类型是 Object,应该是允许添加字符串的,但是因为编译期对类型的检查,导致编译无法通过;

List<Integer> list = new ArrayList<>();
list.add(1234);
list.add("hello")

如果通过反射在 运行时 插入数据,是能够成功被调用并插入的

从获取 add 方法需要传入 Object 类可以再次确认类型已经被替换为 Object;

可以尝试替换成 Integer.class ,会发现抛出 NoSuchMethodException 异常;

List<Integer> list = new ArrayList<>();
Method addMethod = list.getClass().getDeclaredMethod("add", Object.class);
addMethod.invoke(list, "1234");
System.out.println(list.get(0));

// 输出 1234

在运行时取出数据并赋值给不匹配的类型,会抛出 ClassCastException

List<Integer> list = new ArrayList<>();
Method addMethod = list.getClass().getDeclaredMethod("add", Object.class);
addMethod.invoke(list, "1234");
Integer firstElement = list.get(0);
System.out.println(firstElement);

因为编译后的字节码会在赋值处自动做一次 checkcast 类型转换,如果不能成功转换,则会抛出该异常;

换在 JDK1.5 版本前,需要显式使用类型强转;

反射获取类型信息

既然类型擦除了,那泛型的类型信息是否就已经消失了呢?

并不是,从字节码中可以看到保持了带有泛型信息的 Signature

编写一个简单的代码:

public class Main {
    public static void main(String[] args) {
        List<Number> list = new ArrayList<>();
        list.add(1);
        Number a = list.get(0);
    }
}

使用 javap -s -v xxx.class 查看字节码信息:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #7                  // class java/util/ArrayList
         3: dup
         4: invokespecial #9                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_1
        10: invokestatic  #10                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        13: invokeinterface #16,  2           // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        18: pop
        19: aload_1
        20: iconst_0
        21: invokeinterface #22,  2           // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        26: checkcast     #26                 // class java/lang/Number
        29: astore_2
        30: return
      LineNumberTable:
        line 15: 0
        line 16: 8
        line 17: 19
        line 18: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  list   Ljava/util/List;
           30       1     2     a   Ljava/lang/Number;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      23     1  list   Ljava/util/List<Ljava/lang/Number;>;
  • 可以看到,new 的时候只有 ArrayList 这个类,而不是 ArrayList<Number>,说明类型 Number 被擦除了;

  • 但是 Number 类型信息并没有消失,在 LocalVariableTypeTable 地方存在有 Ljava/util/List<Ljava/lang/Number;> ——Number的类型信息;

  • 所以类型擦了,也等于没擦?

  • 同时在取出数据的时候,会进行一个 checkcast 类型转换,将从 ArrayList 取出的数据检查会否能转换为 Number,如果不能则会抛出 ClassCastException

因此可以通过反射来获取这些泛型信息;

Type

java.lang.reflect.Type是Java中所有类型的公共高级接口,代表了Java中的所有类型。

Type 体系中类型的包括(这些类型都实现Type接口):

  • 数组类型(GenericArrayType)

  • 参数化类型(ParameterizedType)

  • 类型变量(TypeVariable)

  • 通配符类型(WildcardType)

  • 原始类型(Class)、基本类型(Class)

而泛型就是 ParameterizedType

桥接方法(多态)

父类 Box 在类型擦除之后,会被替换为 Object;

class Box<T> {
    private T value;

    void setValue(T value) { this.value = value; }

    T getVaule() { return this.value; }
}

// ---------- 类型擦除 ---------------------

class Box {
    private Object value;

    void setValue(Object value) { this.value = value; }

    Object getVaule() { return this.value; }
}

如果子类指定了 Box 父类的泛型类型(如下代码),所期待的父类方法应该是以 String 作为类型的,但是父类被类型擦除了变成 Object;

  • 此时子类 重写 的方法和父类的方法不相同了(子类的是String作为返回值,而父类是 Object),而是变成了 重载(参数不同,返回值不同)

class BoxImpl extends Box<String> {

   @Override
   String getVaule() {
       return super.getVaule();
   }
}

// 所期待的父类,此时重写父类的方法正确 (例子假设)
class Box {
    private String value;

    void setValue(String value) { this.value = value; }

    String getVaule() { return this.value; }
}

重载方法并不能实现 多态,这就导致了多态和类型擦除的冲突,因此JVM使用了 “桥接方法” 来解决这种冲突

使用 javap -c BoxImpl.class 查看字节码,能够发现,BoxImpl 有4个方法,

  • 其中前两个都是代码对应的,

  • 而后面的 Object 相关的方法就是编译器生成的方法,而这些内部的方法调用了对应的 String 相关方法

  • 当多态发生时,调用 Box#getValue(): T 实际上会调用 BoxImpl#getValue(): Object ,进而调用 BoxImpl#getValue(): String

class BoxImpl extends Box<java.lang.String> {
  BoxImpl();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method Box."<init>":()V
       4: return

  java.lang.String getVaule();
    Code:
       0: ldc           #7                  // String something
       2: areturn

  void setValue(java.lang.String);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #9                  // Method Box.setValue:(Ljava/lang/Object;)V
       5: return

  java.lang.Object getVaule();
    Code:
       0: aload_0
       1: invokevirtual #13                 // Method getVaule:()Ljava/lang/String;
       4: areturn

  void setValue(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #19                 // class java/lang/String
       5: invokevirtual #21                 // Method setValue:(Ljava/lang/String;)V
       8: return
}

Java 内部使用了这种桥接方法,解决了多态和类型擦除的冲突问题;(你以为你重写了方法就真的是重写了方法吗)需要生成桥接方法来连接父类和子类;

如果细心可以发现,String getValue() 和 桥接方法 Object getValue() 只有返回值不同,而 Java 中不能把返回值作为判断重载方法的依据的,这是不是就相矛盾了?

如果允许返回值作为判断根据,那编译器不就生成了相同的桥接方法了么?

同时Jvm是支持通过返回类型和参数类型来确定一个方法的;


注意点

基本数据类型不能作为泛型参数

因为类型擦除后是 Object(无指定的话),Object 是无法存储基本数据类型的,只能存储引用类型,

所以只能使用引用类型作为泛型的参数;


无法实例化泛型

下面的例子就是错误的:

class Box<T> {
    T value;

    Box() {
        value = new T(); // 编译错误
    }
}

编译期间无法确定泛型的具体类型(类型被擦除了),导致找不到对应的类字节码文件;

本来是 new Persion() 的实例化代码,类型擦除后变成 new Object() ,很明显是不对的;

可以通过反射来实现实例化:

class Box<T> {
    T value;

    Box(Class<T> clazz) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        value = clazz.getDeclaredConstructor().newInstance();
    }
}

实际上 clazz 提供了对应类的信息


泛型数组

泛型数组不能实例化

T[]类型的数组

指的是 new Xxx[] 的普通数组,这种泛型数据不能实例化;

不能实例化的原因和上面泛型实例化的原因相同,无法确定实例化的类(更准确的应该是无法确定申请内存的大小);

static <T> T[] init(int size) {
    return new T[size];
}

可以通过反射类生成:

public ArrayWithTypeToken(Class<T> type, int size) {
    array = (T[]) Array.newInstance(type, size);
}

或者通过可变接受参数类实现:

static <T> T[] init(T ...arg) {
    return arg;
}

Xxx<T>类型的数组

Java 中是不允许创建确切泛型类型的数组的

List<String>[] strLists = new ArrayList<String>[10]; // 编译错误: Generic array creation

为什么呢?

指定了类型,而类型擦除后就变成了 ArrayList[10] ,此时无论赋值什么都不会报错,比如:

List<Integer> intList = new ArrayList<Integer>();
strLists[0] = intList;
// 因为类型擦除后,数组的类型是 List[], intList 的类型是 List,
// 所以 intList 能赋值给 strLists;

如果取出数据就会自动进行类型转换,于是就会抛出 ClassCastException

这个和泛型用于消除 ClassCastException 的目的相矛盾,因此不允许;

但是若使用类型通配符 <?>,就可以创建数组;

这是因为使用 <?> 在取出的时候需要 显式 进行类型强转(不是自动进行的)

也就是说需要承担类型强转的潜在问题;

List<?>[] commonLists= new ArrayList<?>[10];
List<String> lists = (List<String>) commonLists[0];

异常捕获无法使用泛型

异常捕获无法使用泛型,如果允许了则会出现一系列问题

但是允许抛出泛型类型的异常

class ApiException<T extends Throwable> extends Exception {
	// 编译错误 Generic class may not extend 'java.lang.Throwable'
}

首先要了解 catch 子句的规则:越前出现的异常需要越具体,不能出现相同异常的捕获

例子1:会出现相同的异常捕获子句

try {
    
} catch (ApiException<RuntimeException> e) {
    
} catch (ApiException<IllegalArgumentException> e) {
    
}

//------ 类型擦除后 ------
try {
    
} catch (ApiException e) { 
    
} catch (ApiException e) { // 编译错误 Exception 'ApiException' has already been caught
    
}

例子2:违反越前出现的异常需要越具体

static <T extends Throwable> void catchBlock(Consumer<Integer> consumer) {
    try {
        consumer.accept(10);
    } catch (T e) { // 编译错误:Cannot catch type parameters
        
    } catch (NumberFormatException e) {
        
    }
}

//------ 类型擦除后 ------

static void catchBlock(Consumer<Integer> consumer) {
    try {
        consumer.accept(10);
    } catch (Throwable e) { 
        
    } catch (NumberFormatException e) { // 编译错误:Exception 'java.lang.NumberFormatException' has already been caught
        
    }
}

例子3:允许抛出泛型类型的异常

// 正确用法
static <T extends Throwable> void catchBlock(T customException, Consumer<Integer> consumer) throws T {
    try {
        consumer.accept(10);
    } catch (NumberFormatException e) {
        throw customException;
    }
}