Java type inference: reference is ambiguous in Java 8, but not Java 7
Asked Answered
S

2

27

Lets say we have 2 classes. An empty class Base, and a subclass of this class Derived.

public class Base {}

public class Derived extends Base {}

Then we have a few methods in another class:

import java.util.Collection

public class Consumer {

    public void test() {
        set(new Derived(), new Consumer().get());
    }

    public <T extends Base> T get() {
        return (T) new Derived();
    }

    public void set(Base i, Derived b) {
        System.out.println("base");
    }

    public void set(Derived d, Collection<? extends Consumer> o) {
        System.out.println("object");
    }

}

This compiles and runs successfully in Java 7, but does not compile in Java 8. The error:

Error:(8, 9) java: reference to set is ambiguous
  both method set(Base,Derived) in Consumer and 
  method set(Derived,java.util.Collection) in Consumer match

Why does work in Java 7, but not Java 8? How could <T extends Base> ever match Collection?

Systole answered 11/2, 2015 at 23:55 Comment(6)
Well, it compiles fine for me with Java 8Poker
Eclipse compiles this, but javac cannot. Is it safe to assume you're using javac?Ezarra
@Ezarra Yes. I get this error compiling from the commandline with javac, and with IntelliJ. I'm using java version 1.8.0_25Systole
You can have things extending Base implementing Collection...Tonneau
This is the same issue as that one, except that things have been "improved" in Java 8 (in fact, the spec is unintuitive, but compilers behave correctly according to the spec)Stegosaur
@Jeffrey, for the records: Eclipse 4.4 accepted this program (with an "unchecked" warning!), but starting from 4.5M3 also Eclipse reports the ambiguity.Hypotonic
Z
24

The problem is that the type inference has been improved. You have a method like

public <T extends Base> T get() {
    return (T) new Derived();
}

which basically says, “the caller can decide what subclass of Base I return”, which is obvious nonsense. Every compiler should give you an unchecked warning about your type cast (T) here.

Now you have a method call:

set(new Derived(), new Consumer().get());

Recall that your method Consumer.get() says “the caller can decide what I return”. So it’s perfectly correct to assume that there could be a type which extends Base and implement Collection at the same time. So the compiler says “I don’t know whether to call set(Base i, Derived b) or set(Derived d, Collection<? extends Consumer> o)”.

You can “fix” it by calling set(new Derived(), new Consumer().<Derived>get()); but to illustrate the madness of your method, note that you can also change it to

public <X extends Base&Collection<Consumer>> void test() {
    set(new Derived(), new Consumer().<X>get());
}

which will now call set(Derived d, Collection<? extends Consumer> o) without any compiler warning. The actual unsafe operation happened inside the get method.

So the correct fix would be to remove the type parameter from the get method and declare what it really returns, Derived.


By the way, what irritates me, is your claim that this code could be compiled under Java 7. Its limited type inference with nested method calls leads to treating the get method in a nested invocation context like returning Base which can’t be passed to a method expecting a Derived. As a consequence, trying to compile this code using a conforming Java 7 compiler will fail as well, but for different reasons.

Zworykin answered 12/2, 2015 at 9:54 Comment(0)
F
5

Well, its not as if Java7 is happy to run it. It gives couple of warnings before giving the error:

jatin@jatin-~$ javac -Xlint:unchecked -source 1.7 com/company/Main.java 
warning: [options] bootstrap class path not set in conjunction with -source 1.7
com/company/Main.java:19: error: no suitable method found for set(Derived,Base)
        set(new Derived(), new Consumer().get());
        ^
    method Consumer.set(Base,Derived) is not applicable
      (argument mismatch; Base cannot be converted to Derived)
    method Consumer.set(Derived,Collection<? extends Consumer>) is not applicable
      (argument mismatch; Base cannot be converted to Collection<? extends Consumer>)

com/company/Main.java:28: warning: [unchecked] unchecked cast
        return (T) new Derived();
                   ^
  required: T
  found:    Derived
  where T is a type-variable:
    T extends Base declared in method <T>get()

The issue is this:

set(new Derived(), new Consumer().get());

When we do new Consumer().get(), if we look at signature of get. It returns us a Type T. We know that T somehow extends Base. But we do not know what T is specifically. It can be anything. So if we cant specifically decide what T is, then how can compiler?

One way to tell compiler, is by hardcoding and telling it specifically by: set(new Derived(), new Consumer().<Derived>get());.

The reason above is extremely extremely (repeated intentionally) dangerous, is when you try doing this:

class NewDerived extends Base {
     public String getName(){return "name";};
}
NewDerived d = new Consumer().<NewDerived>get();
System.out.println(d.getName());

In Java7 (or any Java version), it will throw exception at runtime:

Exception in thread "main" java.lang.ClassCastException: com.company.Derived cannot be cast to com.company.NewDerived

Because get returns an object of type Derived but you have mentioned to compiler that it is of NewDerived. And it cant convert Derived to NewDerived rightly. This is why it shows a warning.


As per the error, now we understand what is wrong with new Consumer().get(). It's type is something that extends base. Doing set(new Derived(), new Consumer().get()); looks for a method that takes arguments as Derived (or any super class of it), something that extends Base.

Now both of your methods qualify for the first argument. As per the second argument something that extends base, again both are eligible as that something can be Derived or extend Derived or extend Collection. This is why Java8 throws Error.

As per Java7, its type inference is bit weaker. So it tries doing something similar,

Base base = new Consumer().get();
set(new Derived(), base);

And again, it cannot find the right method which takes Derived, Base as arguments. Hence it throws error but for different reason:

    set(new Derived(), new Consumer().get());
    ^
method Consumer.set(Base,Derived) is not applicable
  (argument mismatch; Base cannot be converted to Derived)
method Consumer.set(Derived,Collection<? extends Consumer>) is not applicabl e
  (argument mismatch; Base cannot be converted to Collection<? extends Consu mer>)

PS: Thanks To Holger, for pointing out incompleteness of my answer.

Fotina answered 12/2, 2015 at 17:34 Comment(3)
Actually, the Java 8 compiler isn’t that smart. In fact, new Consumer().<NewDerived>get() can be compiled without problems. And the compiler error of the questioner doesn’t tell about how wrong the unchecked operation is, it just says that the method invocation is ambiguous. That implies that removing one of the two set methods (regardless of which one!) will “fix” the compiler error, even if the result is that the wrong method (expecting a Collection) will be called. Or, well, attempted, as the cast from Derived to Collection will fail then.Zworykin
Btw. the Java 7 compiler doesn’t give warnings, it gives errors. Only the first line in your output is a warning.Zworykin
@Zworykin I completely agree. Thanks for pointing out. There was something else in my mind and I tried pointing a different thing. Thanks. Edited the answer. Thanks again!Fotina

© 2022 - 2024 — McMap. All rights reserved.