java之集合类框架的简要知识点:泛型的类型擦除

时间:2022-06-01 17:51:39

这里想说一下在集合框架前需要理解的小知识点,也是个人的肤浅理解,不知道理解的正不正确,请大家多多指教。
这里必须谈一下java的泛型,因为它们联系紧密,我们先看一下这几行代码:

Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println( c1 + "==" + c2 + " is " + (c1 == c2) );

  

这里主要想测试一下这两个类是不是相等的,根据我之前的认识,这应该是不相等的,但是运行输出的结果是:

class java.util.ArrayList==class java.util.ArrayList is true

根据打印结果显然c1 和 c2都叫class java.util.ArrayList ,所以后面是否相等跟的是 true,居然和我的预期完全相反的,
这是怎么回事呢,这就要提到java泛型里的类型擦除(type erasue),这黑科技的提出肯定是有多方面原因的,我摘录了一些我了解到的原因。
让我们一步步了解:

一、保持代码版本的兼容性
在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的"任意化","任意化"带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。所以后期引入了泛型,泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。但是需要保持代码的兼容性

二、目前了解到通常情况下编译器处理泛型有两种方式,java选择了类型擦除,两种方式如下:
1.Code specialization。在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。例如,针对一个泛型list,可能需要 针对string,integer,float产生三份目标代码。
2.Code sharing。对每个泛型类只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。
C++中的模板(template)是典型的Code specialization实现。C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中integer list和string list是两种不同的类型。这样会导致代码膨胀(code bloat),
Code specialization另外一个弊端是在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code sharing方式处理泛型的主要原因。
Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。

类型擦除的来世大概说了,我们看看类型擦除大概是怎么回事:
我们都知道重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义,根据java编辑器处理泛型的方式,上面代码编译完成后应该会变成这样:

Class c1 = new ArrayList<Object>().getClass();
Class c2 = new ArrayList<Object>().getClass();
System.out.println( c1 + "==" + c2 + " is " + (c1 == c2) );

  

可以看出来,无论类是什么,经过编译器后再虚拟机里运行都是相同的类,所以结果是相等的。

所以总结一下类型擦除引起一些奇怪的现象,包括:

  • 泛型类并没有自己独有的Class类对象。比如上面测试的例子。
  • 静态变量是被泛型类的所有实例所共享的。比如声明为MyClass<T>的类,不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
  • 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。