泛型 Generic 类型擦除引起的问题及解决方法

时间:2021-09-10 10:14:10

因为种种原因,Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型,这样虽然不会有类型膨胀的问题,但是也引起了许多新的问题。所以,Sun对这些问题作出了许多限制,避免我们犯各种错误。

1、先检查,再编译,以及检查编译的对象和引用传递的问题

既然说类型变量会在编译的时候擦除掉,那为什么我们往ArrayList<String> arrayList=new ArrayList<String>();所创建的数组列表arrayList中,不能使用add方法添加整形呢?不是说泛型变量Integer会在编译时候擦除变为原始类型Object吗,为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
java是如何解决这个问题的呢?java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。
举个例子说明:
public static  void main(String[] args) {
ArrayList<String> arrayList=new ArrayList<String>();
arrayList.add("123");
arrayList.add(123);//编译错误
}
在上面的程序中,使用add方法添加一个整形,在eclipse中,直接就会报错,说明这就是在编译之前的检查。因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该运行任意引用类型的添加的。可实际上却不是这样,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

那么,这么类型检查是针对谁的呢?我们先看看参数化类型与原始类型的兼容
以ArrayList举例子
//以前的写法:
ArrayList arrayList = new ArrayList();
//现在的写法:
ArrayList<String> arrayList = new ArrayList<String>();
如果是与以前的代码兼容,各种引用传值之间,必然会出现如下的情况:
ArrayList<String> arrayList1 = new ArrayList(); //可以实现与完全【使用泛型参数】一样的效果
arrayList1.add("1");//编译通过
arrayList1.add(1);//编译错误
String str = arrayList1.get(0);//返回类型就是String ArrayList arrayList2 = new ArrayList<String>();//可以实现与完全【不使用泛型参数】一样的效果
arrayList2.add(1);//编译通过
Object object = arrayList2.get(0);//返回类型就是Object new ArrayList<String>().add("11");//编译通过
new ArrayList<String>().add(22);//编译错误
String string = new ArrayList<String>().get(0);//返回类型就是String
这样写都是没有错误的,不过都会有个编译时警告。
arrayList1的警告为:
Type safety: The expression of type ArrayList needs unchecked conversion to conform to ArrayList<String> .
类型安全性:ArrayList类型的表达式需要未经检查的转换才能符合ArrayList <String>
arrayList2的警告为:
ArrayList is a raw type. References to generic type ArrayList<E> should be parameterized .
ArrayList是一个原始类型。 对泛型类型ArrayList <E>的引用应该被参数化

所以上述arrayList1可以实现与完全使用泛型参数一样的效果,而arrayList2则完全没效果。因为new ArrayList()只是在内存中开辟一个存储空间,可以存储任何的类型对象,而真正涉及类型检查的是它的引用,因为我们是使用它的引用来调用它的方法,比如说调用add()方法。

通过上面的例子,我们可以明白,类型检查就是针对引用的,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

从这里,我们可以再讨论下泛型中参数化类型为什么不考虑继承关系
在Java中,像下面形式的引用传递是不允许的:
ArrayList<String> arrayList=new ArrayList<Object>();//编译错误  Type mismatch: cannot convert from ArrayList<Object> to ArrayList<String>
ArrayList<Object> arrayList=new ArrayList<String>();//编译错误 Type mismatch: cannot convert from ArrayList<String> to ArrayList<Object>

我们先看第一种情况,将第一种情况拓展成下面的形式:
ArrayList<Object> arrayList = new ArrayList<Object>();
ArrayList<String> arrayList2 = arrayList;//编译错误 Type mismatch: cannot convert from ArrayList<Object> to ArrayList<String>
我们先假设第二行代码编译没错。那么当我们使用arrayList2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的),可是它里面实际上存放的是 Object 类型的对象,这样,就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷。

再看第二种情况,将第二种情况拓展成下面的形式:
ArrayList<String> arrayList = new ArrayList<String>();
ArrayList<Object> arrayList2 = arrayList;//编译错误 Type mismatch: cannot convert from ArrayList<String> to ArrayList<Object>
没错,这样的情况比第一种情况好的多,最起码在我们用arrayList2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢?泛型出现的原因,就是为了解决类型转换的问题,我们使用了泛型,到头来还是要自己强转,违背了泛型设计的初衷,所以java不允许这么干!再说,你如果又用arrayList2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢?

所以,要格外注意,泛型中的引用传递的问题。

2、自动类型转换

因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?看下ArrayList和get方法:
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
看以看到,在return之前,会根据泛型变量进行强转。

3、类型擦除与多态的冲突和解决方法

现在有这样一个泛型类:
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
然后我们想要一个子类继承它
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
} @Override
public Date getValue() {
return super.getValue();
}
}
在这个子类中,我们设定父类的泛型类型为Pair<Date>,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:
将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型,所以,我们在子类中重写这两个方法一点问题也没有。实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?

分析:
实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
我们再来看子类重写父类的 setValue 方法中参数的类型:父类的类型是Object,而子类的类型是Date,父类和子类中参数类型不一样,这如果是在普通的继承关系中,根本就不会是重写,而是重载,也就不能使用@Override标签。
我们在一个main方法测试一下:
DateInter dateInter = new DateInter();
dateInter.setValue(new Date());//编译正确
dateInter.setValue(new Object());//编译错误 The method setValue(Date) in the type DateInter is not applicable for the arguments (Object)
然而,从上面测试代码可以看出,确是是重写了,而不是重载了。因为如果是重载,那么子类中肯定有两个setValue方法,一个是参数Object类型,一个是参数Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。

为什么会这样呢?
后面省略1000字......

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

不能用类型参数替换基本类型。就比如,没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

5、运行时类型查询

举个例子:
ArrayList<String> arrayList = new ArrayList<String>();
System.out.println(arrayList instanceof ArrayList<?>);//true
System.out.println(arrayList instanceof ArrayList);//true
//System.out.println(arrayList instanceof ArrayList<Object>);//编译错误
//System.out.println(arrayList instanceof ArrayList<String>);//编译错误
下面的两种方式会提示如下错误:
Cannot perform instanceof check against parameterized type ArrayList<Object/String>.
无法对参数化类型ArrayList <Object/String>执行instanceof检查。
Use the form ArrayList<?> instead since further generic type information will be erased at runtime
使用形式ArrayList <?>,因为进一步的泛型类型信息将在运行时被擦除
因为类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。所以,运行时进行类型查询的时候,使用下面的两种方式是错误的。

6、异常中使用泛型的问题

1、不能抛出也不能捕获泛型类的对象。事实上,泛型类扩展Throwable都不合法。例如:下面的定义将不会通过编译:
public class Problem<T> extends Exception{...}
为什么不能扩展Throwable,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,在看下面的定义:
try{
}catch(Problem<Integer> e1){
}catch(Problem<Number> e2){
}
类型信息被擦除后,那么两个地方的catch都变为原始类型Object,那么也就是说,这两个地方的catch变的一模一样,就相当于下面的这样
try{
}catch(Problem<Object> e1){
}catch(Problem<Object> e2){
}
这个当然就是不行的。就好比,catch两个一模一样的普通异常,不能通过编译一样:

2、不能在catch子句中使用泛型变量
public static <T extends Throwable> void doWork(Class<T> t){
try{
}catch(T e){ //编译错误
}
}
因为泛型信息在编译的时候已经变为原始类型,也就是说上面的T会变为原始类型Throwable,那么如果可以在catch子句中使用泛型变量,那么,下面的定义呢:
public static <T extends Throwable> void doWork(Class<T> t){
try{
}catch(T e){ //编译错误
}catch(IndexOutOfBounds e){
}
}
根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面就违背了这个原则。即使你在使用该静态方法的使用T是ArrayIndexOutofBounds,在编译之后还是会变成Throwable,ArrayIndexOutofBounds是IndexOutofBounds的子类,违背了异常捕获的原则。所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。

但是在异常声明中可以使用类型变量。下面方法是合法的。
public static<T extends Throwable> void doWork(T t) throws T{
try{
}catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
}
上面的这样使用是没问题的。

7、数组

这个不属于类型擦除引起的问题。
不能声明参数化类型的数组。如:
Pair<String>[] table = new Pair<String>[10]; //Cannot create a generic array of Pair<String>
如果需要收集参数化类型对象,直接使用ArrayList:ArrayList<Pair<String>>最安全且有效。

8、泛型类型的实例化

不能实例化泛型类型。如,
T t= new T(); //ERROR  Cannot instantiate the type T

9、类型擦除后的冲突

当泛型类型被擦除后,创建条件不能产生冲突。如在Pair类中添加下面的equals方法:
public boolean equals(T value) {
//Name *: The method equals(T) of type Pair<T> has the same erasure as equals(Object) of type Object but does not override it
//名称冲突:Pair <T>类型的方法equals(T)具有与Object类似的equals(Object)相同的擦除,但不覆盖它
return false;
}
擦除后方法 boolean equals(T) 变成了方法 boolean equals(Object) ,这与 Object 的 equals 方法是冲突的!当然,补救的办法是重新命名引发错误的方法。

泛型规范说明提及另一个原则:"要支持擦除的转换,需要强行制一个类或者类型变量不能同时成为两个接口的子类,而这两个子类是同一接品的不同参数化"。
下面的代码是非法的:
class A implements Comparable<Integer> {
@Override
public int compareTo(Integer o) {
return 0;
}
} class B extends A implements Comparable<String> {
//The interface Comparable cannot be implemented more than once with different arguments: Comparable<Integer> and Comparable<String>
//接口Comparable不能使用不同的参数多次实现:可比较的<Calendar>和Comparable <GregorianCalendar>
}
B会实现Comparable<Integer>和Compable<String>,这是同一个接口的不同参数化实现。
这一限制与类型擦除的关系并不很明确。非泛型版本:
 class A implements Comparable {//Comparable is a raw type. References to generic type Comparable<T> should be parameterized
@Override
public int compareTo(Object o) {
return 0;
}
} class B extends A implements Comparable {
}
是合法的。

10、泛型在静态方法和静态类中的问题

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
public class Test<T> {
public static T one; //编译错误 Cannot make a static reference to the non-static type T
public static T show(T one){ //编译错误 Cannot make a static reference to the non-static type T
return null;
}
}
因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

但是要注意区分下面的一种情况:
public class Test<T> {
public static <T >T show(T one){//这是正确的
return null;
}
}
因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。