泛型和类型擦除

泛型和类型擦除

一. 泛型

泛型,即参数化类型,把类型当作参数一样传递,增强代码的 类型安全性可重用性可读性与维护性

如 ArrayList< E >即泛型类型,E为类型参数变量。ArrayList< Integer >为参数化的类型,ParameterizedType,Integer为实际类型参数

参数化类型就是把类型当作参数一样去传递,这样设计可以让代码被不同类型变量重用,提高代码利用率和整洁性,在泛型参数出现前,泛型设计如ArrayList等集合类是只维护一个Object引用的数组,这样导致每一次使用都要进行一次强制类型转换,所以加入了参数化类型以后,就用选定的参数来表示类型,而编译器不需要进行强转就可以知道其数据类型。

使用:

  1. 泛型类:允许在声明时定义一个或多个类型参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 定义泛型类,T 是类型参数
    public class Box<T> {
    private T value;

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

    public T getValue() {
    return value;
    }
    }

    // 使用
    Box<String> stringBox = new Box<>();
    stringBox.setValue("Hello");
    System.out.println(stringBox.getValue()); // 输出 "Hello"

    Box<Integer> intBox = new Box<>();
    intBox.setValue(100);
    System.out.println(intBox.getValue()); // 输出 100
  2. 泛型方法:在方法中定义泛型类型,可以用于任何类或接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class GenericMethod {
    // 定义一个泛型方法,T 是类型参数
    public <T> void printArray(T[] array) {
    for (T element : array) {
    System.out.println(element);
    }
    }
    }

    // 使用
    GenericMethod gm = new GenericMethod();
    Integer[] intArray = {1, 2, 3};
    String[] strArray = {"A", "B", "C"};
    gm.<Integer>printArray(intArray); // 输出 1 2 3
    gm.<String>printArray(strArray); // 输出 A B C
  3. 泛型接口:类似于泛型类,接口中也可以定义类型参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public interface GenericInterface<T> {
    T getValue();
    void setValue(T value);
    }

    public class GenericClass<T> implements GenericInterface<T> {
    private T value;

    @Override
    public T getValue() {
    return value;
    }

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

    // 使用
    GenericClass<String> genericString = new GenericClass<>();
    genericString.setValue("Hello");
    System.out.println(genericString.getValue());

实现一个泛型类并不像表面那么简单,大部分开发人员只会停留在使用封装好的泛型类这一层面上,那么如何去构造一个泛型类呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class Pair<T,U> {
private T first;
private U second;

public Pair() {
this.first = null;
this.second = null;
}

public Pair(T first, U second) {
this.first = first;
this.second = second;
}

public T getFirst() {
return first;
}

public void setFirst(T first) {
this.first = first;
}

public U getSecond() {
return second;
}

public void setSecond(U second) {
this.second = second;
}

public static <T> T getMiddle(T...a){//泛型方法
return a[a.length/2];
}

//对类型的约束
public static <T extends Comparable> T min(T[] a){
//T是Comparable的子类型(subtype),extends表示二者关系更接近于继承。
// 这类继承可以一对多(接口情况下),T extends A & B,擦除时会用A来代替T,所以方法较少的接口应放到末尾
if(a == null || a.length == 0) return null;
T min = a[0];
for(int i = 1;i < a.length;i++)
if(min.compareTo(a[i]) > 0) min = a[i];
return min;
}
}

public class Test {
public static void main(String[] args){
//创建泛型类对象,指定数据类型,可以把泛型类看作工厂类
Pair<Integer,String> pair = new Pair<>();
Pair<String,String> compare = new Pair<>();
String[] strings = {"Mary","had","a","little","lamb"};
System.out.println("min: [" + findMaxAndMin(strings).getFirst() + "]");
System.out.println("max: [" + findMaxAndMin(strings).getSecond() + "]");
System.out.println("middle: [" + Pair.getMiddle(strings) + "]");
System.out.println("middle: [" + Pair.getMiddle("abc","def","ghi") + "]");
System.out.println("middle: [" + Pair.getMiddle(3.14,1234,0) + "]");
//编译器如何判断泛型方法的调用类型?
System.out.println("middle: [" + Pair.getMiddle("asda",0,null) + "]");
}

public static Pair<String,String> findMaxAndMin(String[] strings){
if(strings == null || strings.length == 0) return null;
String min = strings[0];
String max = strings[0];
for(int i = 1;i < strings.length;i++){
if(strings[i].compareTo(min) < 0) min = strings[i];
if(strings[i].compareTo(max) > 0) max = strings[i];
}
return new Pair<>(min,max);
}
}

运行结果:

1
2
3
4
5
6
min: [Mary]
max: [little]
middle: [a]
middle: [def]
middle: [1234]
middle: [0]

二. 类型擦除

Java编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理时被擦除,将泛型类型转换为它们的 原始类型(通常是 Object 或限定的上界类型),这个过程即类型擦除

泛型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。

无论何时定义一个泛型类型,都会自动生成一个相应的原始类型-raw type。原始类型的名字就是删去类型参数后的泛型类型名,擦除类型变量,并替换为限定类型,若没有限定类型就替换为Object。如Pair类就替换为 Object first; Object second;等。

2.1 类型擦除的主要过程

在编译期间,Java 会执行以下步骤:

  1. 泛型类型替换:编译器会将泛型类型替换为其 限定的类型Object
    • 如果泛型声明了上限(如 <T extends Number>),编译器会将泛型替换为上限类型。用其最左边界(最顶级的父类型)类型替换。
    • 如果没有指定上限,泛型将被替换为 Object
  2. 插入类型检查和类型转换:在插入泛型类型的地方,编译器会自动插入类型检查和类型转换代码,以确保类型安全性。
  3. 生成字节码:擦除后的代码与非泛型代码基本一致,泛型信息在运行时不可见。

2.2 翻译泛型表达式

程序调用泛型方法时,如果擦除了返回类型,编译器会插入强制类型转换。

1
2
Pair<String,String> pair1 = new Pair<>();
String first = pair1.getFirst();

编译器将其翻译为两条虚拟机指令:

  1. 对原始方法Pair.getFirst调用
  2. 将返回的Object类型强转为String类型

2.3 翻译泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Parent<T> {
public void test(T t){
System.out.println("Parent t[" + t + "]");
}
}

public class Child extends Parent<String> {
public void test(String t){
System.out.println("Child t[" + t + "]");
}
}

Child child = new Child();
Parent<String> parent = child;
parent.test("This is a string");

运行结果:

1
Child t[This is a string]

对于编译器来说子类中擦拭类型后应包含:

1
2
test(Object t)
test(String t)

因为多态的特性,子类调用父类方法,应该在子类中重写,此时并未重写父类方法只是新增了一个不同参数的同名方法,但为什么可以正常使用?

2.4 桥方法

1
2
3
public void test(Object t){
test((String) value);
}

编译器会在子类中生成一个桥方法,也就是对父类方法的重写,此机制用来避免类型擦除和多态发生冲突。

JVM是用返回值+方法名+参数的方式来计算函数签名的,所以编译器就可以借助这一原则来生成一个桥方法。

2.5 总结

  • 虚拟机中没有泛型,只有类和方法。
  • 所有类型参数都用其限定类型替换。
  • 虚拟机生成桥方法来保持多态。
  • 为保证类型安全性,必要时插入强制类型转换。

三. 消除异常检查

通过注解关闭对代码的检查:

1
@SuppressWarnings("unchecked")

四. 泛型的局限和约束

4.1 不能用原始数据类型实例化类型参数

只能用其包装类型,如Integer实例化整型数据,原因是类型擦除后Object不能存储原始数据类型的数据。

4.2 运行时类型查询只适用于原始类型

不能使用instanceof判断对象是否属于某个泛型类型时。

4.3 不能创建参数化类型的数组

禁止的原因大概是,类型擦除后table的类型转换为Object[],当存储不同类型的元素时(非Parent元素),会抛出异常,但泛型会破坏这一机制,Parent< Integer >也可以通过数组的检查,但最终会导致类型错误,所以编译器禁止直接创建参数化类型的数组,但可以声明通配类型的数组,这种使用可能是不安全的,如下列注释1,最好的方法是使用ArrayList< Parent< String > >。

1
2
3
4
5
6
7
if(child instanceof Parent<String>);//Error
Parent<String>[] table = new Parent<String>[10]; //Error
Parent<String>[] table1 = (Parent<String>[]) new Parent<?>[10];
Object[] objects1 = table1;
objects1[0] = "Hello";//Error: ArrayStoreException
objects1[0] = new Parent<Integer>();//1
//此时调用objects1[0]中的T的String方法会抛出异常

但如果定义一个参数可变的方法,向其传递泛型类型实例呢?

1
2
3
4
5
6
7
public static <T> void addAll(Collection<T> col,T...ts){
for(T t : ts) col.add(t);
}
Collection<Parent<String>> table = new ArrayList<>();
Parent<String> p1 = new Parent<>();
Parent<String> p2 = new Parent<>();
addAll(table,p1,p2);

这种情况下编译器仅仅是提示了一个Varargs警告,可以通过给调用方法处加@SuppressWarnings(“unchecked”),或用注释@SafeVarargs标注被调用方法。

1
2
3
4
@SafeVarargs
public static <T> void addAll(Collection<T> col,T...ts){
for(T t : ts) col.add(t);
}

所以也可以通过@SafeVarargs来通过传递可变参数构建一个泛型数组,但同样如果用Parent< Integer >之类的去给数组赋值,在处理此元素时难免会导致异常。

4.4 不能实例化类型变量

不能像创建对象那样去实例化泛型,如new T();等。在Java8之后可以通过构造器表达式来实现。

1
2
3
4
5
6
7
8
9
public class Parent<T> {
private T first;
private T second;

public Parent() {
this.first = new T();//Error
this.second = new T();//Error
}
}

该方法接收一个Supplier< T >,即函数式接口,表示一个无参数且返回类型为T的函数。

1
2
3
4
5
6
7
8
public static <T> Parent<T> makeParent(Supplier<T> constr){
return new Parent<>(constr.get(),constr.get());
}

public Parent(T first, T second) {
this.first = first;
this.second = second;
}

通过反射来构造泛型对象。

1
2
3
4
5
6
7
8
public static <T> Parent<T> makeParent(Class<T> constr){
try {
// return new Parent<>(constr.newInstance(),constr.newInstance());
return new Parent<>(constr.getDeclaredConstructor().newInstance(),constr.getDeclaredConstructor().newInstance());
}catch (Exception ex){
return null;
}
}

调用方法。

1
Parent<String> p = Parent.makeParent(String.class);

4.5 不能构造泛型数组

不能实例化泛型数组,数组虽然可以填充null,对于数组元素实例是安全的,但数组本身也有类型,此类型会被擦除。

1
2
3
public static <T extends Comparable> T[] minmax(T[] a){
T[] mm = new T[2];//Error
}

类型擦除后,数组变为Comparable[2],如果此数组仅仅存活在类中的私有域内,可以通过声明一个Object[]数组,对元素进行类型转换,ArrayList就是这样设计的。

4.6 泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量。

4.7 不能抛出或捕获泛型类的实例对象

泛型类无法继承Exception或Throwable,catch子句中无法使用类型变量。

五. 泛型类型的继承

1
2
3
Child extends Parent
Child[] ? Parent[]
Pair<Child> ? Pair<Parent>

结果:

1
2
3
4
Child[] children = new Child[10];
Parent[] parents = children;//OK
ArrayList<Child> childList = new ArrayList<>();
ArrayList<Parent> parentList = childList;//Error

即Parent[]和Child[]仍是继承,但ArrayList< Parent >和ArrayList< Child >则没有关系,假设上述合法,会导致什么问题?

破坏类型安全,若创建成功,先赋值给childList元素c1,c2,尝试给数组ArrayList< Parent >添加元素时,则加入p1,p2。那么对于childList其具有父类元素,这显然是不行的。

那么数组呢?如果尝试将p1赋值给parents时,会抛出异常ArrayStoreException

1
2
3
Child[] children = new Child[10];
Parent[] parents = children;
parents[0] = new Parent();

结果:

1
Exception in thread "main" java.lang.ArrayStoreException: generic.Parent

所以泛型类型的关系并不会影响到泛型类间的关系

不过参数化类型永远可以转换为原始类型,如ArrayList< String >到List< String >


参考:

🔗《Java核心技术-卷Ⅰ》

🔗《Java核心技术》卷1翻译好差,有时间和机会还是看原文好一点,这么老的书更新了这么多版本了,没想到翻译还是这么差劲。*

🔗《Java虚拟机规范》

🔗泛型就这么简单