Java核心技术第12章(3)

来源:互联网 发布:ubuntu luvit 编辑:程序博客网 时间:2024/06/05 06:27

12.5    泛型代码和虚拟机

    虚拟机没有泛型类型对象--所有对象都属于普通类.无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type).原始类型的名字就是删除类型参数后的泛型类型名.擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object).
    例如,Pair<T>的原始类型如下所示:
public class Pair{    private Object first;    private Object second;    public Pair(Object first, Object second)    {        this.first = first;        this.second = second;    }    ...}
    因为T是一个无限定的变量,所以直接用Object替换.
    在程序中可以包含不同类型的Pair,例如,Pair<String>或Pair<GregorianCalendar> .而擦除类型后就变成原始的Pair类型了.
    注释:就这点而言,Java泛型与C++模板有很大的区别.C++中每个模板的实例化产生不同的类型,这一现象称为"模板化膨胀",Java不存在这个问题的困扰.
    原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换.例如,类Pair<T>中的类型变量没有显式的限定,因此,原始类型用Object替换T .假定声明了一个不同的类型.
public class Interval<T extends Comparable & Serializable> implements Serializable{    private T lower;    private T upper;    ...    public Interval(T first, T second)    {        if (first.compareTo(second) <= 0)        {            lower = first;            upper = second;        }        else        {            lower = second;            upper = second;        }    }}
    原始类型Interval如下所示:
public class Interval implements Serializable{    private Comparable lower;    private Comparable upper;    ...    public Interval(Comparable first, Comparable second)    {...}}

12.5.1  翻译泛型表达式

    当程序调用泛型方法时,如果擦掉返回类型,编译器插入强制类型转换.例如,下面这个语句序列
Pair<Employee> buddies = ...;Employee buddy = buddies.getFirst();
    擦除getFirst的返回类型后将返回Object类型.编译器自动插入Employee的强制类型转换.也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
    对原始方法Pair.getFirst的调用
    将返回的Object类型强制转换为Employee类型

    当存取一个泛型域时也要插入强制类型转换.假设Pair类的first域和second域都是公有的.表达式:
Employee buddy = buddies.first;
    也会在结果字节码中插入强制类型转换.

12.5.2  翻译泛型方法

    类型擦除也会出现在泛型方法中.程序员通常认为下述的泛型方法
public static <T extends Comparable> T min(T[] a)
    是一个完整的方法族,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
    注意,类型参数T已经被擦除掉了,只留下限定类型Comparable .
    方法的擦除带来了两个复杂问题,看一看下面这个示例:
class DateInterval extends Pair<Date>{    public void setSecond(Date second)    {        if (second.compareTo(getFirst()) >= 0)            super.setSecond(second);    }    ...}
    一个日期区间是一对Date对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值.这个类擦除后变成
class DateInterval extends Pair{    public void setSecond(Date second) { ... }}
    令人感到奇怪的是,存在另一个从Pair继承的setSecond方法,即
public void setSecond(Object second)
    这显然是一个不同的方法,因为它有一个不同类型的参数--Object,而不是Date .然而,不应该不一样.考虑下面的语句序列:
DateInterval interval = new DateInterval(...);Pair<Date> pair = interval;pair.setSecond(aDate);
    这里,希望对setSecond的调用具有多态性,并调用最合适的那个方法.由于pair引用DateInterval对象,所以应该调用DateInterval.setSecond,问题在于类型擦除与多态性发生了冲突.要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法(bridge method):
public void setSecond(Object second) { setSecond((Date)second); }
    要想了解它的工作过程,请仔细地跟踪下列语句的执行:
pair.setSecond(aDate)
    变量pair已经声明为类型Pair<Date>,并且这个类型只有一个简单的方法叫setSecond,即setSecond(Object) .虚拟机用pair引用的对象调用这个方法.这个对象是DateInterval类型的,因而将会调用DateInterval.setSecond(Object)方法.这个方法是合成的桥方法,它调用DateInterval.setSecond(Date),这正是所期望的调用效果.
    桥方法可能变得非常奇怪,假设DateInterval方法也覆盖了getSecond方法:
class DateInterval extends Pair<Date>{    public Date getSecond() { return (Date)super.getSecond().clone();}    ...}
    在擦除的类型中,有两个getSecond方法:
Date getSecond();       // define in DateIntervalObject getSecond();     // overrides the method defined in Pair to call the first method
    不能这样编写Java代码,它们都没有参数,但是,在虚拟机中,用参数类型和返回类型确定一个方法.因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能能够正确地处理这一情况.
    注释:桥方法不仅用于泛型类型.在一个方法覆盖另一个方法时可指定一个更严格的返回类型.例如:
public class Employee implements Cloneable{    public Employee clone() throws CloneNotSupportedException { ... }}
    Object.clone和Employee.clone方法被说成具有协变的返回类型(covariant return types).
    实际上,Employee类有两个克隆方法:
Employee clone();       // defined aboveObject clone();         // synthesized bridge method, overrides object.clone
    合成的桥方法调用了新定义的方法.
    总之,需要记住有关Java泛型转换的事实:
    虚拟机中没有泛型,只有普通的类和方法.
    所有的类型参数都用它们的限定类型替换.
    桥方法被合成来保持多态.
    为保持类型安全性,必要时插入强制类型转换.

12.6    约束与局限性

    在下面几节中,将阐述使用Java泛型时需要考虑的一些限制.大多数限制都是由类型擦除引起的.

12.6.1  不能用基本类型实例化类型参数

    不能用类型参数代替基本类型,因此,没有Pair<double>,只有Pair<Double>.当然,其原因是类型擦除.擦除之后,Pair类含有Object类型的域,而Object不能存储 double 值.
    这的确令人烦恼.但是,这样做与Java语言中基本类型的独立状态相一致.这并不是一个致命的缺陷--只有8种基本类型,当包装器类型不能接受替换时,可以使用独立的类和方法处理它们.

12.6.2  运行时类型查询只适用于原始类型

    虚拟机中的对象总有一个特定的非泛型类型.因此,所有的类型查询只产生原始类型.例如:
if (a instanceof Pair<String>)  // error
    实际上仅仅测试a是否是任意类型的一个Pair .下面的测试同样如此:
if (a instanceof Pair<T>)       // error
    或强制类型转换:
Pair<String> p = (Pair<String>)a;
    要记住这一风险,无论何时使用 instanceof 或涉及泛型类型的强制类型转换表达式都会看到一个编译器警告.
    同样的道理,getClass方法总是返回原始类型.例如:
Pair<String> stringPair = ...;Pair<Employee> employeePair = ...;if (stringPair.getClass() == employeePair.getClass())   // they are equal
    其比较的结果是 true,这是因为两次调用getClass都将返回Pair.class .

12.6.3  不能创建参数化类型的数组

    不能实例化参数化类型的数组,例如:
Pair<String>[] table = new Pair<String>[10];        // error
    擦除之后,table的类型是Pair[],可以把它转换为Object[].
Object[] objarray = table;
    数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常:
objarray[0] = "hello";
    不过对于泛型类型,擦除会使得这种机制无效,以下赋值:
objarray[0] = new Pair<Employee>();
    能够通过数组存储检查,不过仍然会导致一个类型错误.出于这个原因,不允许创建参数化类型的数组.
    需要说明的是,只是不允许创建这些数组,而声明类型为Pair<String>[]的变量是合法的.

12.6.5  不能实例化类型变量

    不能使用像 new T(...),new T[...]或T.class这样的表达式中的类型变量.例如下面的Pair<T>构造器就是非法的:
public Pair() { first = new T(); second = new T(); }    // error
    类型擦除将T改变为Object,而且本意肯定不希望调用 new Object().

12.7    泛型类型的继承规则

    无论S与T有什么联系(甚至是父类与子类的关系),通常,Pair<S>与Pair<T>没有什么联系.

0 0
原创粉丝点击