Java核心技术卷I:基础知识(原书第8版):12.5 泛型代码和虚拟机

铁文整理

12.5 泛型代码和虚拟机

    虚拟机没有泛型类型对象——所有对象都属于普通类。在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为在1.0虚拟机上运行的类文件!这个向后兼容性在Java泛型开发的后期被放弃了。如果使用Sub的编译器来编译使用_Java泛型的代码,结果类文件将不能在5.0之前的虚拟机上运行。

    注释:如果想拥有泛型的优势,同时又保留旧的虚拟机字节码兼容性,请查阅:http://sourceforge.net/projects/retroweaverRetroweaver程序重写类文件以便与旧的虚拟机兼容。

    无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object)。

    例如,Pair<T>的原始类型如下所示:

public class Pair {

    public Pair(Object first, Object second) {

        this.first = first;

        this.second = second;

    }

 

    public Object getFirst() {

        return first;

    }

 

    public Object getSecond() {

        return second;

    }

 

    public void setFirst(Object newValue) {

        first = newValue;

    }

 

    public void setSecond(Object newValue) {

        second = newValue;

    }

 

    private Object first;

    private Object second;

}

    因为T是一个无限定的变量,所以直接用Object替换。

    结果是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。

    在程序中可以包含不同类型的Pair,例如,Pair<String>Pair<GregorianCalendar>。而擦除类型后就变成原始的Pair类型了。

    C++注释:就这点而言,Java泛型与C++模板有很大的区别。C++中每个糢板的实例化产生不同的类型,这一现象称为“糢板代码膨胀。Java不存在这个问题的困扰。

    原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。例如,类Pair<T>中的类型变量没有显式的限定,因此,原始类型用Object替换T。假定声明了一个不同的类型。

public class Interval<T extends Comparable & Serializable> implements Serializable {

    public Interval(T first, T second) {

        if (first.compareTo(second) <= 0) {

            lower = first;

            upper = second;

        } else {

            lower = second;

            upper = first;

        }

    }

 

    private T lower;

    private T upper;

}

    原始类型Interval如下所示:

public class Interval implements Serializable {

    public Interval(Comparable first, Comparable second) {

        ...

    }

 

    private Comparable lower;

    private Comparable upper;

}

    注释:读者可能想要知道切换限定:class Interval<Comparable& Serializable>会发生什么。如果这样做,原始类型用Serializable替换T,而编译器在必要时要向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。

12.5.1 翻译泛型表达式

    当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如,下面这个语句序列

        Pair<Employee> buddies = ...;

        Employee buddy = buddies.getFirst();

    擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

  • 对原始方法Pair.getFirst的调用。

  • 将返回的Object类型强制转换为Employee类型。

    当存取一个泛型域时也要插入强制类型转换。假设Pair类的first域和second域都是公有的(也许这不是一种好的编程风格,但在Java中是合法的)。表达式Employee buddy = buddies.first;也会在Java中插入强制类型转换。

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; // OK—assignment to superclass

        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();

    }

}

    在擦除的类型中,有两个getSecond方法:

    Date getSecond() // defined in DateInterval

    Object getSecond() // defined in Pair

    不能这样编写Java代码(在这里,具有相同参数类型的两个方法是不合法的)。它们都没有参数。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。

    注释:桥方法不仅用于泛型类型,第5章已经讲过,从Java SE 5.0开始,在一个方法覆盖另一个方法时可以指定一个更严格的返回类型。例如,

public class Employee implements Cloneable {

    public Employee clone() throws CloneNotSupportedException { ... }

}

    Object.cloneEmployee.clone方法被说成具有协变的返回类型(covariant return types)。实际上,Employee类有两个克隆方法:

    Employee clone() // defined above

    Object clone() // synthesized bridge method, overrides Object.clone

    合成的桥方法调用了新定义的方法。

    总之,需要记住有关Java泛型转换的事实:

  • 虚拟机中没有泛型,只有普通的类和方法。

  • 所有的类型参数都用它们的限定类型替换。

  • 桥方法被合成来保持多态。

  • 为保持类型安全性,必要时插入强制类型转换。

12.5.3 调用遗留代码

    许多Java代码编写于Java SE 5.0之前。如果泛型类不能与这些代码互操作,就得不到广泛的应用。幸运的是,可以直接将泛型类与遗留的API中原始的对应概念放在—起使用。

    下面看一个具体的示例。要想设置一个JSlider标签,可以使用方法:

    void setLabelTable(Dictionary table)

    在第九章中,使用下述代码填充标签表:

           Dictionary<Integer, Component> labelTable = new Hashtable<Integer, Component>();

           labelTable.put(0, new JLabel("A"));

           labelTable.put(20, new JLabel("B"));

           ……

           slider.setLabelTable(labelTable);

    Java SE 5.0中,Dictionary和Hashtable类被转换成泛型类。因此,可以用Dictionary<Integer, Component>取代原始的Dictionary。但是,当将Dictionary<Integer, Component>对象传递给setLabelTable时,编译器会发出一个警告。

        Dictionary<Integer, Component> labelTable = ...;

        slider.setLabelTable(labelTable); // WARNING

    毕竟,编译器无法确定setLabelTable可能会对Dictionary对象做什么操作。这个方法可能会用字符串替换所有的关键字,这就打破了关键字类型为整型的承诺,未来的操作有可能会产生强制类型转换的异常。

    这个警告对操作不会产生什么影响,最多考虑一下JSlider有可能用Dictionary对象做什么就可以了。在这里十分清楚,JSIider只阅读这个信息,因此可以忽略这个警告。

    现在,看一个相反的情形,由一个遗留的类得到一个原始类型的对象。可以将它赋给一个参数化的类型变量,当然,这样做会看到一个警告。例如,

           Dictionary<Integer, Component> labelTable = slider.getLabelTable(labelTable); // WARNING

    这就行了。再看一看警告,确保标签表已经包含了IntegerComponent对象,当然,从来也不会有绝对的承诺。恶意的编码者可能会在滑块中设置不同的Dictionary。然而,这种情况并不会比Java SE 5.0之前的情况更槽。最差的情况就是程序抛出一个异常。

    在査看了警告之后,可以利用注释(annotation)使之消失。注释必须放在生成这个警告的代码所在的方法之前,如下:

    @SuppressWarnings("unchecked")

    public void configureSlider() { ... }

    遗憾的是,这个注释也关闭了对方法内部代码的栓査。将潜在不安全的代码分隔在一些不同的方法中是一个不错主意,这样可以更加容易地进行査看。

    注释:Hashtable类是抽象类Dictionary的具体子类。自从DictionaryHashtable类被Java SE 1.2Map接口和HashMap类取代以来,它们都被标记为“废弃”。尽管从表面上看,它们仍然是有效的。毕竟,JSlider类是在Java SE 1.3中增加的。那时程序员不知道Map类吗?这会给在不远的未来程序员接受泛型带来希望吗?这就是与遗留代码保持一致的方式。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值