泛型和类型擦除

泛型

  泛型,即“参数化类型”。

  如ArrayList< E >即泛型类型,E为类型参数变量

  ArrayList< Integer >为参数化的类型,ParameterizedType,Integer为实际类型参数

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

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

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编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是
被擦除,这个过程即类型擦除。

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

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

  如Pair类就替换为 Object first; Object second;等。


类型擦除的主要过程

  1. 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
  2. 移除所有的类型参数。

翻译泛型表达式

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

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

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

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

翻译泛型方法

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)

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


桥方法

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

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

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


总结

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

消除异常检查

1
@SuppressWarnings("unchecked")

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


泛型的局限和约束

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

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


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

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


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

  禁止的原因大概是,类型擦除后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 >之类的去给数组赋值,在处理此元素时难免会导致异常。


 不能实例化类型变量

  不能像创建对象那样去实例化泛型,如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);

 不能构造泛型数组

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

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

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


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

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


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

  泛型类无法继承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虚拟机规范》

https://segmentfault.com/a/1190000014120746

因博客主未标明不可转载或引用,若内容涉及侵权请及时告知,我会尽快修改和删除相关内容