Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。
什么是泛型,有什么作用?
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。 与非泛型代码相比,使用泛型有三大优点: 更健壮(在编译时进行更强的类型检查)、 更简洁(消除强转,编译后自动会增加强转)、 更通用(代码可适用于多种类型)* 适用于多种数据类型执行相同的代码(代码复用)
什么是类型擦除机制?
将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
Java编译器中
先检查代码中泛型的类型(类型检查就是编译时完成的),
然后在进行类型擦除,
再进行编译。
类型擦除的具体步骤?
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
1:消除类型参数声明,即删除<>及其包围的部分 (见下图)
根据类型参数的上下界推断并替换所有的类型参数为原生态类型:
如果类型参数是无限制通配符或没有上下界限定则替换为Object,
如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)
2:为了保证类型安全,必要时插入强制类型转换代码
3:自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”
编辑
如何证明类型的擦除呢?
ArrayList list1 = new ArrayList();
list1.add("abc");
ArrayList list2 = new ArrayList();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass()); // true
说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
* 通过反射添加其它类型元素
ArrayList list = new ArrayList();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd")
这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型
如何理解泛型的编译期检查?
ArrayList list = new ArrayList();
list.add("123");
list.add(123);//编译错误
泛型变量的使用,是会在编译之前检查,真正涉及类型检查的是它的引用, 也就是 针对前半段 ArrayList list, 所以这里会报错。
因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。
类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测。
不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测。
我们举例说明:
// list1 是string 类型
ArrayList list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误
String str1 = list1.get(0); //返回类型就是String
// list2 是Object 类型
ArrayList list2 = new ArrayList();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型就是Object
// 下面两个都 是string 类型
new ArrayList().add("11"); //编译通过
new ArrayList().add(22); //编译错误
String str2 = new ArrayList().get(0); //返回类型就是String
ArrayList list1 = new ArrayList();
ArrayList list2 = list1; //编译错误
当我们使用list2引用用get()方法取值的时候,返回的都是String类型的对象,可是它里面实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException,
所以这里编译是无法通过的。
如何理解基本类型不能作为泛型类型?
比如,我们没有ArrayList,只有ArrayList, 为何?
因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储int值,只能引用Integer的值。
如何理解泛型类型不能实例化?
不能实例化泛型类型, 这本质上是由于类型擦除决定的:
T test = new T(); // ERROR
因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。 如果我们确实需要实例化一个泛型,应该如何做呢?可以通过反射实现:
static T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
T obj = clazz.newInstance();
return obj;
}
泛型数组:能不能采用具体的泛型类型进行初始化?
Java 的泛型数组初始化时数组类型不能是具体的泛型类型,只能是通配符的形式,因为具体类型会导致可存入任意类型对象,在取出时会发生类型转换异常,会与泛型的设计思想冲突,而通配符形式本来就需要自己强转,符合预期
泛型数组:如何正确的初始化泛型数组实例?
我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class componentType, int length) 方法来创建一个具有指定类型和维度的数组
public ArrayWithTypeToken(Class type, int size) {
array = (T[]) Array.newInstance(type, size);
}
如何理解泛型类中的静态方法和静态变量?
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
public class Test2 {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
//这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T
public static T show(T one){ //这是正确的
return null;
}
}
泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建。
如何理解异常中使用泛型?
不能抛出也不能捕获泛型类的对象,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉
不能再catch子句中使用泛型变量,因为泛型信息在编译的时候已经变为原始类型,也就是说上面的T会变为原始类型Throwable
List 与 List有什么区别?
List 是原生类型,可以添加或访问元素,不具备编译期安全性,而 List 其实是 List的缩写,是协变型的(可引出协变型的特点与限制);从语义上,List 表明使用者清楚变量是类型安全的,而不是因为疏忽而使用了原生类型 List。