类型擦除

chalmery Lv2

一 泛型

在JAVA中的泛型,在编译的时候,所有的泛型信息都会被抹去。这个过程成为 类型擦除。这样做的原因是为了兼容老版本。

JAVA中泛型的引入主要是为了解决两个方面的问题:

  • 减少类型转换
  • 解决的时重复代码的编写,能够复用算法,泛化。

以类Dad为例,有一成员是泛型,并且有对应的get,set方法。

1
2
3
4
5
6
7
8
9
10
11
public class Dad<T> {
private T value;

public T getValue() {
return value;
}

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

而在反编译后,代码如下,可以看到类型T全部被Object替换。如果定义的类泛型指定为 <T extend Comparable>,则编译后所有的T替换为Comparable

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Dad{
private Object value;

public Object getValue()
{
return value;
}

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

二 类型擦除产生的特性

1 编译检查根据引用确定:

既然编译后类型变为Object,因此赋值的时候传递不同类型的值是否可以呢?答案是不可以,因为编译器在编译前会先检查代码中泛型的类型,不符合条件编译不通过。

1
2
3
4
//正确方式
List<String> list = new ArrayList<String>();
list.add("aaa");
list.add(1);//编译报错

而如下这种方式虽然可以通过编译但是会有编译警告,但我们这么用就没意义了,因为存储的类型实际上是Object,如果要强行转换为String是会出现 类型转换异常的。

1
2
3
4
5
6
7
List list = new ArrayList<String>();
list.add("aaa"); //编译警告,未检查的参数
list.add(1);

for (Object o : list) {
System.out.println((String) o); //到第二个参数的时候抛出 类型转换异常
}

从上面的例子可以看出,编译器在检查泛型是否合格的时候确实是根据 引用检查的

2 不允许引用传递时改变类型

既然前面我们知道了编译器检查泛型是否合格是根据 引用来决定是否通过的,那么我们引用传递下换成不同的类型是否是可以的呢? 答案是不可以的,泛型的出现就是为了尽量减少类型转换,这样写代码也就失去了意义。

1
2
List<Object> list1 = new ArrayList<>();
List<String> list2 = list1; //编译报错

3 集合会自动类型转换

因为在编译的时候都变为了Object,而我们获取后为何还是我们制定的类型呢?以ArrayList为例,在获取元素前已经做了类型转换,我们不再需要进行类型转换了。

1
2
3
E elementData(int index) {
return (E) elementData[index]; //转换为指定的类型
}

4 桥方法解决与多态的冲突

有一个泛型父类如下:

1
2
3
4
5
6
7
8
9
10
11
public class Dad<T> {
private T value;

public T getValue() {
return value;
}

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

有子类继承它,并且指定泛型类型,我们会发现这样写 @Override注解是通过的,也就是满足重写。但是父类编译后是Object,而我们子类的两个方法是String类型,因为方法重写的规则是 方法的参数是必须是相同类型的,因此这里的set方法其实并不满足重写规则的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Sub extends Dad<String>{
public Sub() {
}

@Override
public String getValue() {
return "hello world";
}

@Override
public void setValue(String value) {
System.out.println("设置value");
}
}

将子类反编译后会发现,多出来两个对应的方法。可以看到这两个方法调用了我们重写的方法,实际上这两个方法才满足 @Override,这就是 桥方法,编译后通过这个方法解决了泛型类重写的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Sub extends Dad {
public String getValue() //重写的get方法
{
return "hello world";
}

public volatile Object getValue() //生成的桥方法
{
return getValue(); //调用重写的方法
}

public void setValue(String value) //重写的set方法
{
System.out.println("设置value");
}

public volatile void setValue(Object obj) //生产的桥方法
{
setValue((String)obj); //调用重写的set方法
}
}

5 泛型类型变量不能是基本数据类型

因为类型擦除后所有的泛型关键字都是要替换成Object或者其子类,反正一定要是引用类型。如果要使用基本类型存储数据可以使用相应的包装类。

6 集合的instanceof编译不通过

因为编译后类型被擦除,无论是 List<String>还是 List<Integer>都变成了 List,因此下面的语句在编译时候是不通过的:

1
2
ArrayList<String> arrayList = new ArrayList<String>();
if( arrayList instanceof ArrayList<String>) //编译不通过

7 静态成员或者方法无法声明为泛型

因为泛型类型是创建对象的时候才确定是什么类型的,而静态属性或者方法不需要使用对象调用,无法确定泛型是什么类型的。

而下面这种情况例外:因为方法show的返回值类型是由方法参数决定的,返回值类型就是参数类型。

1
2
3
4
5
6
public class Demo<T> {  

public static <T> T show(T t){ //编译正确
return null;
}
}

三 如何保存泛型信息

既然泛型信息擦除了,那么反射应该是获取不到类型信息的吧,但是还是能获取到

有一个泛型父类,在构造方法中通过反射获取类型信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class EntityHandler<T> {
public EntityHandler() {
//获取class对象
Class<?> clazz = this.getClass();
//获取类型
Type genericSuperclass = clazz.getGenericSuperclass();
System.out.println(genericSuperclass); //EntityHandler<User>

//获取泛型信息
ParameterizedType t = (ParameterizedType)clazz.getGenericSuperclass();
Type[] ts = t.getActualTypeArguments();
for (Type type : ts) {
System.out.println(type); //class User
}
}
}

子类赋值为了User类型

1
2
3
4
5
public class UserHandler extends EntityHandler<User>{
public static void main(String[] args) {
new UserHandler();
}
}

控制台打印:

1
2
test.base.generic.EntityHandler<test.base.generic.User>
class test.base.generic.User

答案就是在编译为字节码文件的时候,泛型信息通过 Signature保存了下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
public test.base.generic.UserHandler();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method EntityHandler."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltest/base/generic/UserHandler;
}
Signature: #12 // /EntityHandler<Ltest/User;
1
2
3
4
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();

System.out.println(l1.getClass() == l2.getClass());
  • 标题: 类型擦除
  • 作者: chalmery
  • 创建于 : 2021-10-02 00:00:00
  • 更新于 : 2021-10-02 00:00:00
  • 链接: https://github.com/chalmery/2260828129/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论