Domanda

I am reading 'Java Generics and Collections' section 8.4. The author defines the following code while trying to explain Binary Compatibility:

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;
    }
}

and then talks about making the Name interface and SimpleName class generic and leaving the ExtendedName as is. As a result the new code is:

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!
    }
}

The author describes the result of such an action as following:

Say that we generify Name and SimpleName so that they define compareTo(Name), but that we do not have the source for ExtendedName. Since it defines only compareTo(Object), client code that calls compareTo(Name) rather than compareTo(Object) will invoke the method on SimpleName (where it is defined) rather than ExtendedName (where it is not defined), so the base names will be compared but the extensions ignored.

However when I make only Name and SimpleName generic I get a compile time error and not what the author describes above. The error is:

name clash: compareTo(Object) in NameHalfMovedToGenerics.ExtendedName and compareTo(T) in Comparable have the same erasure, yet neither overrides the other

And this is not the first time I am facing such an issue - earlier while trying to read Sun documentation on erasure I faced a similar issue where my code doesn't show the same result as described by the author.

Have I made a mistake in understanding what the author is trying to say?

Any help will be much appreciated.

Thanks in advance.

È stato utile?

Soluzione

This is an example of a problem that can occur under separate compilation.

The main subtlety with separate compilation is that, when a caller class is compiled, certain information is copied from the callee into the caller's class file. If the caller is later run against a different version of the callee, the information copied from the old version of the callee might not match exactly the new version of the callee, and the results might be different. This is very hard to see by just looking at source code. This example shows how the behavior of a program can change in a surprising way when such a modification is made.

In the example, Name and SimpleName were modified and recompiled, but the old, compiled binary of ExtendedName is still used. That's really what it means by "the source code for ExtendedName is not available." When a program is compiled against the modified class hierarchy, it records different information than it would have if it were compiled against the old hierarchy.

Let me run through the steps I performed to reproduce this example.

In an empty directory, I created two subdirectories v1 and v2. In v1 I put the classes from the first example code block into separate files Name.java, SimpleName.java, and ExtendedName.java.

Note that I'm not using the v1 and v2 directories as packages. All these files are in the unnamed package. Also, I'm using separate files, since if they're all nested classes it's hard to recompile some of them separately, which is necessary for the example to work.

In addition I renamed the main program to Test1.java and modified it as follows:

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

In v1 I compiled everything and ran 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

Now, in v2 I placed the Name.java and SimpleName.java files, modified using generics as shown in the second example code block. I also copied in v1/Test1.java to v2/Test2.java and renamed the class accordingly, but otherwise the code is the same.

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

This shows that the result of m.compareTo(n) is different after Name and SimpleName were modified, while using the old ExtendedName binary. What happened?

We can see the difference by looking at the disassembled output from the Test1 class (compiled against the old classes) and the Test2 class (compiled against the new classes) to see what bytecode is generated for the m.compareTo(n) call. Still in 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
...

When compiling Test1, the information copied into the Test1.class file is a call to compareTo(Object) because that's the method the Name interface has at this point. With the modified classes, compiling Test2 results in bytecode that calls compareTo(Name) since that's what the modified Name interface now has. When Test2 runs, it looks for the compareTo(Name) method and thus bypasses the compareTo(Object) method in the ExtendedName class, calling SimpleName.compareTo(Name) instead. That's why the behavior differs.

Note that the behavior of the old Test1 binary does not change:

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

But if Test1.java were recompiled against the new class hierarchy, its behavior would change. That's essentially what Test2.java is, but with a different name so that we can easily see the difference between running an old binary and a recompiled version.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top