Code Complete
Code Complete

Reputation: 3206

Method ambiguous in a hierarchy of two generic classes while method actually given same-type argument

Why method f() is ambiguous in Derived?

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

class Derived<S extends CharSequence> extends Base<String> {
    void f(S arg) { }  // compiles OK so far !!!
}

    Derived<String> obj = new Derived<>();
    obj.f("hi"); // c.ERR: The method f(String) is ambiguous for the type Derived<String>

BUT:

Derived<StringBuffer> obj = new Derived<>();
obj.f("hi"); // OK! Overloaded methods resolved?
obj.f(new StringBuffer()); // OK! Overloaded methods resolved?

void f(String) is inherited from Base (void f(Object) at runtime), same void f(String) is generated in Derived (void f(Object) at runtime).

Please help me to pinpoint when and where is the conflict?

So far I tend to think that proper method to call is chosen from all possible overloaded methods at compile-time only (!), and Derived extends Base means to the compiler that at compile-time Derived has two methods with same signatures: Base.f(String) and Derived.f(String) which makes f("hi") call ambiguous (both overloaded versions apply).

Overriding in this case is not possible (@Override in Derived really gives compile error) because same signature (same argument) cannot be guaranteed, and it is just one lucky coincidence for ...extends Base where override could be possible, but JLS would flag compile error.

The fact that real runtime signatures of methods (as seen by JVM after compilation) are void Base.f(Object) and void f(CharSequence) is totally irrelevant to overload resolution because that resolution happens only at compile time!

Java Language Specification:

When a method is invoked (§15.12), the number of actual arguments (and any explicit type arguments) and the compile-time types of the arguments are used, at compile time, to determine the signature of the method that will be invoked (§15.12.2). If the method that is to be invoked is an instance method, the actual method to be invoked will be determined at run time, using dynamic method lookup (§15.12.4).

P.S. Are Base.f() and Derived.f() always overloaded? But how then? As void f(Object) at runtime vs void f(CharSequence) at runtime? If they are overridden in case of new Derived, then are they overloaded or overridden depending on what type parameter Derived given during instantiation? Is that possible?

Upvotes: 2

Views: 287

Answers (2)

Alanpatchi
Alanpatchi

Reputation: 1199

For the given case of Base class,

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

Your Derived class,

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

doesn't override the f() method, but rather overloads the f() method because of type erasure.

Hence, the Derived class contains 2 overloads of f(),

  • one from the parent Base class: void f(Object arg)
  • another from its own class: void f(CharSequence arg)

Since, in java Generics provide only compilte-time type safety, compiler makes full use of this to infer correct overload of f() on a parameterized Derived<SomeSubClassOfCharSequence> depending on the argument passed.

This is what happened in the case of

Derived<StringBuffer> obj = new Derived<>();
obj.f("hi"); // OK! Overloaded methods resolved?
obj.f(new StringBuffer()); // OK! Overloaded methods resolved?

But, in the case of

Derived<String> obj = new Derived<>();
obj.f("hi");

The compiler is not able to infer the one overload over the other, hence the ambiguity.

To answer yourquestions:

  1. Are Base.f() and Derived.f() always overloaded?: This depends on whether the type-erasured result of these declared versions of f() produce different method signatures.
  2. There are scenarios in which compiler itself generates a new override of the type-erasured Base.f() in the Derived.f() method, aka bridge methods. But your case is not one of these scenarios.

Upvotes: 1

mentallurg
mentallurg

Reputation: 5207

The definition of the class Base means that method f() can accept parameters of any type. It can be not only String, but also Integer, Long, and actually an instance of any class. What class, it depends on generic parameter used when an instance of class Base is created.

The definition of the class Derived allows to pass instances of any sub-classes of CharSequence.

It is very important that the definition of Derived does not mean that its generic parameter S is somehow related to the generic parameter T of the parent class. Such definition means, that the method f() in Derived does not override method f() from the parent class. Thus, the compiler sees two methods.

What could you do? It depends on what is your goal. But one solution may be following:

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

class Derived<S extends CharSequence> extends Base<S> {
    void f(S arg) { }
}

Derived<String> obj = new Derived<>();
obj.f("hi");

Upvotes: 2

Related Questions