Why List<String> can't simultaneously be argument of a Base-class generic method and Derived-class non-generic method?

Viewed 186

Why List<String> can't simultaneously be argument of a Base-class generic method and Derived-class non-generic method?

 class Base {
     <T> void f(List<String> arg) {}
 }

 class Derived extends Base {
     void f(List<String> arg) {}

     // above is compile ERROR: method f(List<String>) of type Derived has the same erasure 
     //as f(List<String>) of type Base but does not override it
 }

I don't understand that compiler message and the reason for compilation error.

There is no problem with return type:

class Base {
     <T> List<String> f() { return null; }
}

class Derived extends Base {     
    List<String> f() { return null; }  // perfectly valid as return-type
}
2 Answers

Wow, that was a fun search.

This depends on the exact specification of when a method is said to override another method.

The relevant part is in §8.4.8.1 Overriding (by Instance Methods) of the Java Language Specification (I'm using the one for Java 14).

An instance method mC declared in or inherited by class C, overrides from C another method mA declared in class A, iff all of the following are true:

[...]

  • The signature of mC is a subsignature (§8.4.2) of the signature of mA.

So let's look at §8.4.2. Method Signature:

Two methods or constructors, M and N, have the same signature if they have the same name, the same type parameters (if any) (§8.4.4), and, after adapting the formal parameter types of N to the the type parameters of M, the same formal parameter types.

The signature of a method m1 is a subsignature of the signature of a method m2 if either:

m2 has the same signature as m1, or

the signature of m1 is the same as the erasure (§4.6) of the signature of m2.

Two method signatures m1 and m2 are override-equivalent iff either m1 is a subsignature of m2 or m2 is a subsignature of m1.

For our case there are a couple thing to note:

  1. The return value is not part of the signature, so it basically gets ignored when deciding if one method overrides another. There are additional constraints based on the return type, but these constraints do not affect if two method override each other, but if the overriding does compile. See §8.4.8.3 Requirements in Overriding and Hiding.

  2. The two signatures in question are not the same because the don't have the same number of type parameters.

  3. They are not subsignatures, because this would require one to be the erasure of the other, but both signatures contain generic types (List<String>). Note that this changes if there are no generics in the method signatures, i.e. if you use List as the parameter or if List<String> only appears in the return value.

=> the method in Derived does not override the one in Base.

But their erasure is the same. Erasure basically removes all type parameters. See §4.6 Type Erasure for the obviously way more complex details. But what matters here is that the erasure of the signatures is: void f(List arg)

This violates a section in §8.4.8.3 Requirements in Overriding and Hiding:

It is a compile-time error if a type declaration T has a member method m1 and there exists a method m2 declared in T or a supertype of T such that all of the following are true:

  • m1 and m2 have the same name.
  • m2 is accessible (§6.6) from T.
  • The signature of m1 is not a subsignature (§8.4.2) of the signature of m2.
  • The signature of m1 or some method m1 overrides (directly or indirectly)has the same erasure as the signature ofm2or some methodm2` overrides (directly or indirectly).

Of course this brings us to the question: why is the weird definition of subsignature used?

This is actually explained in §8.4.2. Method Signature:

The notion of subsignature is designed to express a relationship between two methods whose signatures are not identical, but in which one may override the other. Specifically, it allows a method whose signature does not use generic types to override any generified version of that method. This is important so that library designers may freely generify methods independently of clients that define subclasses or subinterfaces of the library.

Well the problem is with your first declaration being incomplete:

<T> void f(List<String> arg) {}de here

Any generic defined before return type is normally supposed to be used in the method's argument. As it is not used; For the incompleteness, reason compiler sees the method in superclass vs child class as similar first due to the same arguments but as it sees there is a generic involved in superclass which is not involved in child class it fails to determine what it exactly means.

The return types don't play any role for this one for example, try following will still show error:

class Base {
     <T> List<String> f(List<String> arg) { return null; }
}

class Derived extends Base {     
    List<String> f(List<String> arg) { return null; }
}

I would suggest to follow the generics syntax completely in order not to have such odd behaviors.

Related