Overriding with subclass as a parameter and generics: where is it in Java Lang Spec?
I've run into Java code similar to the following:
public interface BaseArg {
}
public class DerivedArg implements BaseArg {
}
public abstract class Base <A extends BaseArg> {
A arg;
void doIt() {
printArg(arg);
}
void prin开发者_运维百科tArg(A a) {
System.out.println("Base: " + a);
}
}
public class Derived extends Base<DerivedArg> {
void printArg(DerivedArg a) {
System.out.println("Derived: " + a);
}
public static void main(String[] args) {
Derived d = new Derived();
d.arg = new DerivedArg();
d.doIt();
}
}
(feel free to split it into files and run it).
This code ends up invoking the Derived printArg. I realize it's the only logical thing to do. However, if I perform "erasure" on the generic Base manually, replacing all occurrences of A with BaseArg, the overriding breaks down. I now get the Base's version of printIt.
Seems like "erasure" is not total - somehow printArg(A a) is not the same as printArg(BaseArg a). I can't find any basis for this in the language spec...
What am I missing in the language spec? It's not really important, but it bugs me :) .
Please note that the derived method is invoked. The question is why, considering their erased signatures are not override-equivalent.
When compiling class Derived, the compiler actually emits two methods: The method printArg(DerivedArg), and a synthetic method printArg(BaseArg), which overrides the superclass method in terms even a virtual machine ignorant of type parameters can understand, and delegates to printArg(DerivedArg). You can verify this by throwing an exception in printArt(DerivedArg), while calling it on a reference of type Base, and examining the stack trace:
Exception in thread "main" java.lang.RuntimeException
at Derived.printArg(Test.java:28)
at Derived.printArg(Test.java:1) << synthetic
at Base.doIt(Test.java:14)
at Test.main(Test.java:39)
As for finding this in the Java Language Specification, I first missed it as well, as it is not, as one might expect, specified where overriding or the subsignature relation is discussed, but in "Members and Constructors of Parameterized Types" (§4.5.2), which reveals that formal type parameters of the superclass are syntactically replaced by the actual type parameter in the subclass prior to checking for override equivalence.
That is, override equivalence is not affected by erasure, contrary to popular assumption.
If you do "manual" type erasure, you define the arg instance in BaseArg as type "BaseArg", not type "DerivedArg", so that's resolved to Base's "doIt(BaseArg)" method rather than Derived's "doIt(DerivedArg)" method. If you then alter Derived's method signature to
void printArg( BaseArg a )
from
void printArg(DerivedArg a)
it will print "Derived: arg" as expected.
I believe the behaviour that you encountered is due to the overloading method resolution.
See Java Lang Spec on overloading: link text
And also this wonderful resource on Java Generic regarding the topic.
The printArg
in Derived
does not override the printArg
in Base
. In order for it to override, by JLS 8.4.8.1, the overriding method's signature must be a "subsignature" of the overridden method's. And then by JLS 8.4.2, a subsignature must either have the same argument types (which yours doesn't), or its erasure must be the same (which is also not true).
First of all, you can compile the source code in a single file if you get rid of the "public" declarations for all of the classes/interfaces except "Derived".
Second, go ahead and do the type erasure by hand. Here's what I got when I did it:
interface BaseArg {}
class DerivedArg implements BaseArg {}
abstract class Base {
BaseArg arg;
void doIt() {
printArg(arg);
}
void printArg(BaseArg a) {
System.out.println("Base: " + a);
}
}
public class Derived extends Base {
void printArg(BaseArg a) {
System.out.println("Derived: " + a);
}
public static void main(String[] args) {
Derived d = new Derived();
d.arg = new DerivedArg();
d.doIt();
}
}
In the generic version of the code, it may look like methods Derived.printArg
and Base.printArg
have different signatures. However, if that were the case, then Derived.printArg
could never be invoked by doIt
. The type-erased version of the code makes it clear that Derived.printArg
overrides Base.printArg
, so doIt
polymorphically calls the right method.
How is printArg
in Base defined after your manual erasure ?
void printArg(BaseArg a) {
so, printArg(Derived a)
does NOT override it and will not be called.
EDIT:
if you use the Override annotation in Derived, you'll get an error doing the manual erasure.
精彩评论