无法重现Type Erasure示例的结果

时间:2013-11-26 22:48:14

标签: java generics type-erasure

我正在阅读“Java Generics and Collections”第8.4节。作者在尝试解释二进制兼容性时定义了以下代码:

interface Name extends Comparable {
    public int compareTo(Object o);
}
class SimpleName implements Name {
    private String base;
    public SimpleName(String base) {
        this.base = base;
    }
    public int compareTo(Object o) {
        return base.compareTo(((SimpleName)o).base);
    }
}
class ExtendedName extends SimpleName {
    private String ext;
    public ExtendedName(String base, String ext) {
        super(base); this.ext = ext;
    }
    public int compareTo(Object o) {
        int c = super.compareTo(o);
        if (c == 0 && o instanceof ExtendedName)
        return ext.compareTo(((ExtendedName)o).ext);
        else
        return c;
    }
}
class Client {
    public static void main(String[] args) {
        Name m = new ExtendedName("a","b");
        Name n = new ExtendedName("a","c");
        assert m.compareTo(n) < 0;
    }
}

然后讨论如何使Name接口和SimpleName类通用并保持ExtendedName不变。因此,新代码为:

interface Name extends Comparable<Name> {
    public int compareTo(Name o);
}
class SimpleName implements Name {
    private String base;
    public SimpleName(String base) {
        this.base = base;
    }
    public int compareTo(Name o) {
        return base.compareTo(((SimpleName)o).base);
    }
}
// use legacy class file for ExtendedName
class Test {
    public static void main(String[] args) {
        Name m = new ExtendedName("a","b");
        Name n = new ExtendedName("a","c");
        assert m.compareTo(n) == 0; // answer is now different!
    }
}

作者描述了以下行为的结果:

  

假设我们生成了Name和SimpleName,以便他们定义   compareTo(Name),但是我们没有ExtendedName的源代码。既然它定义了   只有compareTo(Object),调用compareTo(Name)而不是compareTo(Object)的客户端代码将调用SimpleName上的方法(定义它的地方)而不是   ExtendedName(未定义),因此基本名称将被比较但是   扩展被忽略。

然而,当我只使Name和SimpleName通用时,我得到编译时错误,而不是作者上面所描述的。错误是:

  

名称冲突:NameHalfMovedToGenerics.ExtendedName中的compareTo(Object)和Comparable中的compareTo(T)具有相同的擦除,但都不会覆盖其他

这不是我第一次遇到这样的问题 - 早在尝试阅读有关擦除的Sun文档时,我遇到了类似的问题,我的代码没有显示与作者描述的相同的结果。

我在理解作者试图说的内容时犯了错误吗?

非常感谢任何帮助。

提前致谢。

1 个答案:

答案 0 :(得分:2)

这是单独编译下可能出现的问题示例。

单独编译的主要细节是,当编译调用者类时,某些信息将从被调用者复制到调用者的类文件中。如果稍后针对被叫方的不同版本运行调用方,则从旧版本的被调用方复制的信息可能与被调用方的新版本不完全匹配,结果可能不同。仅通过查看源代码就很难看到。此示例显示了在进行此类修改时,程序的行为如何以令人惊讶的方式发生变化。

在示例中,NameSimpleName已被修改并重新编译,但仍然使用旧的已编译的ExtendedName二进制文件。这真的意味着“ExtendedName的源代码不可用。”当针对修改后的类层次结构编译程序时,它会记录与针对旧层次结构编译时不同的信息。

让我来完成我为重现这个例子而执行的步骤。

在一个空目录中,我创建了两个子目录v1v2。在v1中,我将第一个示例代码块中的类放入单独的文件Name.javaSimpleName.javaExtendedName.java

请注意,我没有使用v1v2目录作为包。所有这些文件都在未命名的包中。此外,我正在使用单独的文件,因为如果它们都是嵌套类,则很难单独重新编译它们,这对于示例来说是必要的。

此外,我将主程序重命名为Test1.java并将其修改如下:

class Test1 {
    public static void main(String[] args) {
        Name m = new ExtendedName("a","b");
        Name n = new ExtendedName("a","c");
        System.out.println(m.compareTo(n));
    }
}

v1我编译了所有内容并运行了Test1:

$ ls
ExtendedName.java  Name.java  SimpleName.java  Test1.java
$ java -version
java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)
$ javac *.java
$ java Test1
-1

现在,在v2中,我放置了Name.javaSimpleName.java文件,使用泛型修改,如第二个示例代码块所示。我还将v1/Test1.java复制到v2/Test2.java并相应地重命名了该类,但代码是相同的。

$ ls
Name.java  SimpleName.java  Test2.java
$ javac -cp ../v1 *.java
$ java -cp .:../v1 Test2
0

这表明在m.compareTo(n)Name被修改后SimpleName的结果不同,而使用旧的ExtendedName二进制文件。发生了什么事?

我们可以通过查看Test1类(针对旧类编译)和Test2类(针对新类编译)的反汇编输出来查看差异,以查看生成的字节码用于m.compareTo(n)电话。仍在v2

$ javap -c -cp ../v1 Test1
...
29: invokeinterface #8,  2     // InterfaceMethod Name.compareTo:(Ljava/lang/Object;)I
...

$ javap -c Test2
...
29: invokeinterface #8,  2     // InterfaceMethod Name.compareTo:(LName;)I
...

编译Test1时,复制到Test1.class文件中的信息是对compareTo(Object)的调用,因为这是Name接口此时的方法。使用修改后的类,编译Test2会产生调用compareTo(Name)的字节码,因为这就是修改后的Name接口现在具有的内容。当Test2运行时,它会查找compareTo(Name)方法,从而绕过 compareTo(Object)类中的ExtendedName方法,并调用SimpleName.compareTo(Name)代替。这就是行为不同的原因。

请注意旧的Test1 二进制文件的行为不会改变:

$ java -cp .:../v1 Test1
-1

但如果Test1.java针对新的类层次结构进行了重新编译,则其行为会发生变化。这基本上就是Test2.java,但是使用不同的名称,以便我们可以轻松地看到运行旧二进制文件和重新编译版本之间的区别。