Java泛型的类型擦除

 为什么Java的泛型要使用类型擦除?

        从技术来说,Java完全可以不使用类型擦除而直接实现”真泛型”。然而,Java诞生的时候,是没有包括泛型的,10年之后,Java才想实现类似于C++模板的概念,即泛型。由于Java类库是非常宝贵的资源,因此必须保证向后兼容。如果要实现“真泛型”,不仅需要修改 JVM 的源代码,让 JVM 能正确读取和校验泛型信息;而且为了兼容,需要为原本不支持泛型的 API 都添加相应的泛型 API。这样的修改工作量是无法想象的。因此Java设计者采取了 “类型擦除” 这种机制作为折中的实现方式。

什么是类型擦除?

       在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉,这个过程就称为类型擦除。

        我们以一个例子来验证Java的类型擦除机制。对于下面的代码,最后输出的结果为true。    

ArrayList<Integer> intergerList = new ArrayList<Integer>();
ArrayList<String> stringList = new ArrayList<String>(); 

System.out.println(intergerList.getClass() == stringList.getClass()); // true
    

我们定义的两个列表,一个是String泛型,一个是Integer泛型,但是它们的getClass()方法得到的类信息居然是一样的。这说明泛型类型StringInteger在运行之前就被擦除掉了,在运行时无法知道定义时的具体类型。

        在类型擦除后的实际类型是什么呢?根据上面所说的向后兼容,擦除后的实际类型应该能被之前没有泛型时的Java程序接受。因此Java将具体的类型擦除为原始类型。原始类型就是擦除泛型信息,最后在字节码中的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供。类型变量擦除时,使用其限定类型替换(无限定时用Object替换)。

        举例来说,如果我们定义下面这样一个带泛型的类:

public class Node<T> {
    private T data;
    private Node<T> next;
    public Node(T data, Node<T> next) {    
        this.data = data;
        this.next = next;
    }
    public T getData() {
        return data; 
    }
    // ...
}

那类型擦除后,它的实际定义应该类似于:

public class Node {
    private Object data;
    private Node next;
    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }
    public Object getData() { 
        return data; 
    }
    // ...
}

所有的泛型T都被替换为Object。

类型擦除带来的问题

1.运行时的类型检测问题:由于类型变量会在运行前被擦除掉,那如何检测到向ArrayList<String>中添加非String类型的对象这种类型错误呢?

解决方法:利用编译器进行静态检查。编译器会先检查代码中的泛型类型,如果存在类型不匹配,会直接报错,只有在类型匹配时才能进行后续的类型擦除。比如:

List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts;   // compiler error
myNums.add(3.14);
static long sum(List<Number> numbers) {
    long summation = 0;
    for(Number number : numbers) {
        summation += number.longValue();
    }
    return summation;
}

List<Integer> myInts = Arrays.asList(1,2,3,4,5);
List<Long> myLongs = Arrays.asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
sum(myInts); 
sum(myLongs);   // compiler error
sum(myDoubles);

2.返回泛型类型的函数怎么办?所有的泛型类型都会被替换为原始类型,那返回泛型类型的函数实际上返回的是原始类型的对象,难道还需要手动的类型转化吗?

解决方法:自动生成类型转换。ArrayList的get方法源代码如下:

public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
}

return之前,会根据泛型的类型进行强转。即使泛型信息会被擦除,但是会将(E) elementData[index]编译为(Date) elementData[index]。因此不需要自己手动进行强转,在类型擦除时会自动地在结果字节码中插入强制类型转换。

3.不能使用泛型数组。如果我们假设在 Java 中可以创建泛型数组,那么看如下代码:

List<String>[] s = new ArrayList<String>[1];
List<Integer> i = Arrays.asList(1);
Object[] o = s;
o[0] = i

Java的类型系统规定,子类数组是父类数组的子类,因此Object[] o = s;是正确的。如果我们假设可以创建泛型数组,那上述代码没有任何问题。但是最后一行代码将 List<Integer> 类型的对象赋给了List<String>类型的数组,这个问题在编译时无法发现,只能在运行时出现问题。然而,类型擦除机制要求编译器能够检测所有类型不匹配的问题。因此Java中禁止创建泛型数组,这样编译器就可以检测所有的类型不匹配问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值