Java核心技术第12章(3)

12.5    泛型代码和虚拟机

    虚拟机没有泛型类型对象--所有对象都属于普通类. 无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type).原始类型的名字就是删除类型参数后的泛型类型名.擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object).
    例如,Pair的原始类型如下所示:
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或Pair .而擦除类型后就变成原始的Pair类型了.
    注释:就这点而言,Java泛型与C++模板有很大的区别.C++中每个模板的实例化产生不同的类型,这一现象称为"模板化膨胀",Java不存在这个问题的困扰.
    原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换.例如,类Pair中的类型变量没有显式的限定,因此,原始类型用Object替换T .假定声明了一个不同的类型.
public class Interval 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 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 min(T[] a)
    是一个完整的方法族,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
    注意,类型参数T已经被擦除掉了,只留下限定类型Comparable .
    方法的擦除带来了两个复杂问题,看一看下面这个示例:
class DateInterval extends Pair
{
    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 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,并且这个类型只有一个简单的方法叫setSecond,即setSecond(Object) .虚拟机用pair引用的对象调用这个方法.这个对象是DateInterval类型的,因而将会调用DateInterval.setSecond(Object)方法.这个方法是合成的桥方法,它调用DateInterval.setSecond(Date),这正是所期望的调用效果.
    桥方法可能变得非常奇怪,假设DateInterval方法也覆盖了getSecond方法:
class DateInterval extends Pair
{
    public Date getSecond() { return (Date)super.getSecond().clone();}
    ...
}
    在擦除的类型中,有两个getSecond方法:
Date getSecond();       // define in DateInterval
Object 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 above
Object clone();         // synthesized bridge method, overrides object.clone
    合成的桥方法调用了新定义的方法.
    总之,需要记住 有关Java泛型转换的事实:
    虚拟机中没有泛型,只有普通的类和方法.
    所有的类型参数都用它们的限定类型替换.
    桥方法被合成来保持多态.
    为保持类型安全性,必要时插入强制类型转换.

12.6    约束与局限性

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

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

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

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

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

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

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

12.6.5  不能实例化类型变量

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

12.7    泛型类型的继承规则

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

你可能感兴趣的:(java,java核心技术-基础知识)