详解Java泛型(二)之类型擦除

来源:互联网 发布:python区分单双引号吗 编辑:程序博客网 时间:2024/05/22 10:52

1. 概述

其实Java中的泛型是伪泛型,什么意思呢?就是说它并不是一直都存在的。Java泛型的处理几乎都在编译器中进行,在生成的字节码文件(.class文件)中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,然后编译器在编译的时候去掉,这个过程就是类型擦除

比如下面这段代码使用到了泛型,当list.get(0)的时候不用显示强转变成String类型,在没有使用泛型的时候就需要显示强转一下。

import java.util.ArrayList;import java.util.List;public class ArrayListDemo {    public static void main(String[] args) {        List<String> list = new ArrayList<String>();        list.add("demo");        String str = list.get(0);    }}

然而,在编译以后,它还是给你强转了。我把上面的代码编译后再反编译一下,上面的代码就变成了下面这个样子

import java.util.ArrayList;import java.util.List;public class ArrayListDemo{    public ArrayListDemo()    {    }    public static void main(String args[])    {        List list = new ArrayList();        list.add("demo");        String str = (String)list.get(0);    }}

可以看见,编译器不仅自动帮我添加上了无参构造方法,而且把List的类型变量擦除了,从后面的list.get(0)还是得强转成String类型可以看出。

那么现在问题来了,类型变量被擦除后,泛型类或者说泛型方法它会变成怎样呢?下面就要说一下它的擦除规则。

2. 类型擦除规则

无论什么时候定义一个泛型类型,都会自动提供一个相应的原始类型(raw type),原始类型的名字就是删去类型参数后泛型类型名。如Pair<T>这个泛型类的原型类型就是Pair

那还有一个问题,像上面那个例子,类型变量被擦除后,在字节码中真正的类型是什么?在类型变量被擦除(crased)后,就会用限定类型(无限定的变量用Object)来代替。

例如,我有一个Pair泛型类,代码如下

public class Pair<T> {    private T t1;    private T t2;    public T getT1() {        return t1;    }    public void setT1(T t1) {        this.t1 = t1;    }    public T getT2() {        return t2;    }    public void setT2(T t2) {        this.t2 = t2;    }}

我将这个类编译后再反编译一下,这个类就变成了下面这个样子

public class Pair{    public Pair()    {    }    public Object getT1()    {        return t1;    }    public void setT1(Object t1)    {        this.t1 = t1;    }    public Object getT2()    {        return t2;    }    public void setT2(Object t2)    {        this.t2 = t2;    }    private Object t1;    private Object t2;}

可以看到类型参数T就被Object替换掉了,所以第一个例子中 String str = (String)list.get(0);其实是将Object类型强转成String类型,因为List和Pair的类型变量都没有被限定,所以就用Object来替代了。

下面再来看看有限定的情况。还是Pair类,我将它的类型变量限定为实现了Comparable接口的类,代码如下

public class Pair<T extends Comparable> {    private T t1;    private T t2;    public T getT1() {        return t1;    }    public void setT1(T t1) {        this.t1 = t1;    }    public T getT2() {        return t2;    }    public void setT2(T t2) {        this.t2 = t2;    }}

编译后再反编译就会变成下面这个样子

public class Pair{    public Pair()    {    }    public Comparable getT1()    {        return t1;    }    public void setT1(Comparable t1)    {        this.t1 = t1;    }    public Comparable getT2()    {        return t2;    }    public void setT2(Comparable t2)    {        this.t2 = t2;    }    private Comparable t1;    private Comparable t2;}

可以看到类型变量T被限定类型Comparable替代了。

如果有多个限定类型呢?比如说泛型类Pair<T extends Comparable & Serializable>,如果有多个限定类型的话,会使用第一个限定类型来替换类型变量。如将上面的Pair<T extends Comparable>改为Pair<T extends Serializable & Comparable>,那么反编译后的代码就会是这样子

import java.io.Serializable;public class Pair{    public Pair()    {    }    public Serializable getT1()    {        return t1;    }    public void setT1(Serializable t1)    {        this.t1 = t1;    }    public Serializable getT2()    {        return t2;    }    public void setT2(Serializable t2)    {        this.t2 = t2;    }    private Serializable t1;    private Serializable t2;}

那么这里就有个建议,就是大家在有多个限定类型的时候,把标志接口(没有方法的接口)如上面的Serializable放到最后面,这样子效率会高一点。为什么呢?我用一个例子说明一下,还是上面那个类,不过我加了一个getMax方法,里面有用到Comparable的compareTo方法。

import java.io.Serializable;public class Pair<T extends Serializable & Comparable> {    private T t1;    private T t2;    public T getT1() {        return t1;    }    public void setT1(T t1) {        this.t1 = t1;    }    public T getT2() {        return t2;    }    public void setT2(T t2) {        this.t2 = t2;    }    public T getMax(){        return t1.compareTo(t2) > 0 ?t1:t2;    }}

反编译后

import java.io.Serializable;public class Pair{    public Pair()    {    }    public Serializable getT1()    {        return t1;    }    public void setT1(Serializable t1)    {        this.t1 = t1;    }    public Serializable getT2()    {        return t2;    }    public void setT2(Serializable t2)    {        this.t2 = t2;    }    public Serializable getMax()    {        return ((Comparable)(Comparable)t1).compareTo(t2) <= 0 ? t2 : t1;    }    private Serializable t1;    private Serializable t2;}

可以看出,在getMax方法中做了类型强转,如果将Comparable接口放到限定类型的前面,将Serializable接口放到后面,就是这样子Pair<T extends Comparable & Serializable>,因为会用第一个限定类型即Comparable去替换类型变量,就不会有这种问题,getMax方法就是这样子的

   public Comparable getMax()    {        return t1.compareTo(t2) <= 0 ? t2 : t1;    }

3. 桥接方法

现在我有一个Person泛型类,里面有个age成员变量和setAge方法,注意现在age的类型还不知道

public class Person<T> {    private T age;    public void setAge(T age) {        this.age = age;    }    public T getAge() {        return age;    }}

然后有个Student类继承了Person类,并且确定了T的类型为Integer,并且重写了setAge方法

public class Student extends Person<Integer>{    @Override    public void setAge(Integer age) {        if(age > 0)            super.setAge(age);    }}

然后我将Student类反编译

public class Student extends Person{    public Student()    {    }    public void setAge(Integer age)    {        if(age.intValue() > 0)            super.setAge(age);    }    public volatile void setAge(Object obj)    {        setAge((Integer)obj);    }}

大家有没有发现多了个setAge(Object obj)方法,很明显这个方法与setAge(Integer age),因为它们的参数列表不同。至于为什么会有volatile关键字在我也不清楚,麻烦知道的朋友能说一下。那么为什么会多出一个这样子的方法呢?

大家思考一下下面的代码

public class Main {    public static void main(String[] args) {        Student student = new Student();        Person<Integer> person = student;        person.setAge(123);    }}

这段代码是没有问题的,就是多态性的展示。下面就要说一下它的执行过程。

  1. 创建了一个Student对象
  2. 然后用Person类型的变量person引用Student对象
  3. 因为变量person是Person类型的,而Person类型在编译后只有一个简单的方法setAge(Object),但是虚拟机引用的对象是Student类型的,因而会调用Student.setAge(Object)方法,这个方法就是桥接方法,在这个方法里面调用了Student.setAge(Integer)方法,而这正是我们想要的。

然而如果让Student类再重写Person类的getAge方法的时候就会发生很诡异的事情,反编译后是这样子的

public class Student extends Person{    public Student()    {    }    public void setAge(Integer age)    {        if(age.intValue() > 0)            super.setAge(age);    }    public Integer getAge()    {        return (Integer)super.getAge();    }    public volatile void setAge(Object obj)    {        setAge((Integer)obj);    }    public volatile Object getAge()    {        return getAge();    }}

还没发现问题所在吗?有两个getAge方法呀,就是返回值和修饰符不同,编译器是根据方法签名(方法名加参数列表)来判断重载的,所以这根本不能重载呀,只有方法名相同并且参数列表不同才是重载呀,对于编译器来说这特么的就是一样的方法!为什么可以这样子,如果直接这样子写的话编译是通过不了的呀!

通过上面的情况可以看出编译器可能会产生两个仅返回类型和修饰符不同的方法字节码,而且虚拟机能够处理这种情况

更多关于桥接方法的介绍大家可以去这里看看http://blog.csdn.net/timheath/article/details/53557045

0 0
原创粉丝点击