Java中的泛型和类型擦除


整理自http://irfen.iteye.com/blog/1888312
http://blog.csdn.net/lonelyroamer/article/details/7868820
http://www.cnblogs.com/lwbqqyumidi/p/3837629.html


泛型的实现

通常情况下,一个编译器处理泛型有两种方式:

  • Code specialization。在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。例如,针对一个泛型list,可能需要 针对string,integer,float产生三份目标代码。

  • 2.Code sharing。对每个泛型类只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。
    C++中的模板(template)是典型的Code specialization实现。C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中integer list和string list是两种不同的类型。这样会导致代码膨胀(code bloat),不过有经验的C++程序员可以有技巧的避免代码膨胀。Code specialization另外一个弊端是在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code sharing方式处理泛型的主要原因。
    Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。

类型擦除

类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。
类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List和List等类型,在编译后都会编程List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。

public class Test {
    public static void main(String[] args) {
        ArrayList arrayList1=new ArrayList();
        arrayList1.add("abc");
        ArrayList arrayList2=new ArrayList();
        arrayList2.add(123);
        System.out.println(arrayList1.getClass()==arrayList2.getClass());
    }
}

这个例子证明Java泛型的类型擦除在编译的时候去掉,只剩下原始类型,默认用Object替换

public class Pair<T extends Comparable& Serializable> { 
    ...
}

可以用这个语句来限定变量的原始类型,此时原始类型就用Serializable替换,而编译器在必要的时要向Comparable插入强制类型转换。


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

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

  • 先检查,在编译,以及检查编译的对象和引用传递的问题
    既然说类型变量会在编译的时候擦除掉,那为什么我们往ArrayList arrayList=new ArrayList();所创建的数组列表arrayList中,不能使用add方法添加整形呢?不是说泛型变量Integer会在编译时候擦除变为原始类型Object吗,为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
    java是如何解决这个问题的呢?java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的。
public static  void main(String[] args) {
        ArrayList arrayList=new ArrayList();
        arrayList.add("123");
        arrayList.add(123);//编译错误
    }

在上面的程序中,使用add方法添加一个整形,在eclipse中,直接就会报错,说明这就是在编译之前的检查。因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该运行任意引用类型的添加的。可实际上却不是这样,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

  • 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
public class Test {  
    public static void main(String[] args) {  

        //  
        ArrayList arrayList1=new ArrayList();  
        arrayList1.add("1");//编译通过  
        arrayList1.add(1);//编译错误  
        String str1=arrayList1.get(0);//返回类型就是String  

        ArrayList arrayList2=new ArrayList();  
        arrayList2.add("1");//编译通过  
        arrayList2.add(1);//编译通过  
        Object object=arrayList2.get(0);//返回类型就是Object  

        new ArrayList().add("11");//编译通过  
        new ArrayList().add(22);//编译错误  
        String string=new ArrayList().get(0);//返回类型就是String  
    }  
}  

本来类型检查就是编译时完成的。new ArrayList()只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它引用arrayList1 来调用它的方法,比如说调用add()方法。所以arrayList1引用能完成泛型类型的检查。
而引用arrayList2没有使用泛型,所以不行。

  • 泛型中参数化类型不考虑继承关系
ArrayList arrayList1=new ArrayList();//编译错误
ArrayList arrayList1=new ArrayList();//编译错误 
  

都是不允许的,第一个,它里面实际上已经被我们存放了Object类型的对象,这样,就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。
第二种情况,则是存放String的实例,用存放Object的引用指向它,最起码,在我们用arrayList2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。

  • 自动类型转换 在你调用的地方进行checkcast操作
  • 类型擦除与多态
class Pair {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}
class DateInter extends Pair {
    @Override
    public void setValue(Date value) {
        super.setValue(value);
    }
    @Override
    public Date getValue() {
        return super.getValue();
    }
}


//类型擦除后
class Pair {
    private Object value;
    public Object getValue() {
        return value;
    }
    public void setValue(Object  value) {
        this.value = value;
    }
}
        @Override
    public void setValue(Date value) {
        super.setValue(value);
    }
    @Override
    public Date getValue() {
        return super.getValue();
    }

可是由于种种原因,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。那我们怎么去重写我们想要的Date类型参数的方法?于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法

  • 泛型类型变量不能是基本数据类型
  • 运行时类型查询
 if( arrayList instanceof ArrayList) 
  • 类型通配符
    类型通配符上限通过形如Box< ? extends Number>形式定义,相对应的,类型通配符下限为Box< ? super Number>形式,其含义与类型通配符上限正好相反

你可能感兴趣的:(java,jvm,泛型,java,泛型,jvm,类型擦除)