tags:

views:

240

answers:

6

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 printArg(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 :) .

A: 

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.

Steve B.
A: 

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.

DJ
No overloading is taking place, as the signatures are override-equivalent.
meriton
A: 

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.

Carlos Heuberger
If you had tested that - or at least read the question carefully - you'd have known you're wrong.
meriton
well, I tested it (eclipse Galileo) and could reproduce the results described by the OP. If I add the `Override` annotation (done automatically) I get the error that the method does not override... would be some help to known **what** is wrong.
Carlos Heuberger
A: 

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.

Craig Putnam
A: 

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

newacct
Not quite correct. In fact, the signatures are identical by the definition of types of members of generic types, see my answer for details.
meriton
+4  A: 

Dear fellow responders,

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.

meriton
@meriton: great answer, thanks - I will now be able to sleep at night :)
Arkadiy