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.
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.