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.