Ambiguous overload in Java8 - is ECJ or javac right?
Asked Answered
A

2

15

I have the following class:

import java.util.HashSet;
import java.util.List;

public class OverloadTest<T> extends  HashSet<List<T>> {
  private static final long serialVersionUID = 1L;

  public OverloadTest(OverloadTest<? extends T> other) {}

  public OverloadTest(HashSet<? extends T> source) {}

  private OverloadTest<Object> source;

  public void notAmbigious() {
    OverloadTest<Object> o1 = new OverloadTest<Object>(source);
  }

  public void ambigious() {
    OverloadTest<Object> o2 = new OverloadTest<>(source);
  }
}

This compiles fine under JDK 7's javac, as well as eclipse (with compliance set to 1.7 or 1.8). However, attempting to compile under JDK 8's javac, I get the following error:

[ERROR] src/main/java/OverloadTest.java:[18,35] reference to OverloadTest is ambiguous
[ERROR] both constructor <T>OverloadTest(OverloadTest<? extends T>) in OverloadTest and constructor <T>OverloadTest(java.util.HashSet<? extends T>) in OverloadTest match

Note that this error applies only to the constructor invocation in the ambigous() method, not the one in the notAmbiguous() method. The only difference is that ambiguous() is relying on the diamond operator.

My question is this: Is javac under JDK 8 properly flagging an ambiguous resolution, or was javac under JDK 7 failing to catch an ambiguity? Depending on the answer, I need to either file a JDK bug, or an ecj bug.

Australorp answered 6/10, 2014 at 18:30 Comment(4)
I would rather say a JDK bug, since it seems to break backward compatibility!Lustral
I don't remember the details but if I remember correctly there was a bug in JDK 7 where something like this was allowed by mistake, and JDK 8 has stricter checks. So yes, a break in backward compatibility, but on purpose.Osei
For more details see Incompatibilities between JDK 8 and JDK 7 in Oracle's documentation.Osei
This is a bug in the JDK. This is simple overload resolution where the most specific type, ie. OverloadTest, should be chosen.Upas
G
3

In the invocation, when the constructor is called with T set explicitly, there is no ambiguity:

OverloadTest<Object> o1 = new OverloadTest<Object>(source);

Because T is defined at the time of the constructor call, Object passes the ? extends Object check at compile time just fine and there is no problem. When T is set explicitly to Object, the choices for the two constructors become:

public OverloadTest(OverloadTest<Object> other) {}
public OverloadTest(HashSet<Object> source) {}

And in this case, it's very easy for the compiler to choose the first one. In the other example (using the diamond operator) T is not explicitly set, so the compiler first attempts to determine T by checking the type of the actual parameter, which the first option didn't need to do.

If the second constructor was changed to properly reflect what I imagine is the desired operation (that since OverloadTest is a HashSet of Lists of T, then passing in a HashSet of Lists of T should be possible) like so:

public OverloadTest(HashSet<List<? extends T>> source) {}

...then the ambiguity is resolved. But as it currently stands there will be the conflict when you ask the compiler to resolve that ambiguous invocation.

The compiler will see the diamond operator and will attempt to resolve T based on what was passed in and what the various constructors expect. But the way that the HashSet constructor is written will ensure that no matter which class is passed in, both constructors will remain valid, because after erasure, T is always replaced with Object. And when T is Object, the HashSet constructor and the OverloadTest constructor have similar erasures because OverloadTest is a valid instance of HashSet. And because the one constructor doesn't override the other (because OverloadTest<T> does not extend HashSet<T>), it can't actually be said that one is more specific than the other, so it won't know how to make a choice, and will instead throw a compile error.

This only occurs because by using T as a boundary you are enforcing the compiler to do type-checking. If you simply made it <?> instead of <? extends T> it would compile just fine. The Java 8 compiler is stricter about types and erasure than Java 7 was, partially because many of the new features in Java 8 (like interface defender methods) required them to be a little bit more pedantic about generics. Java 7 was not correctly reporting these things.

Goddard answered 9/10, 2014 at 4:2 Comment(1)
This answer doesn't show how JLS requires a compiler to flag ambiguity. Being more specific is not defined in terms of overriding (see JLS 15.12.2.5 and JLS 18.5.4 (new in Java 8)). Hence I believe this answer to be incorrect.Manipular
M
0

Sorry to spoil the party, but overload resolution in Java is more complex than the comments and answer seen so far.

Both constructors are applicable:

OverloadTest(OverloadTest<? extends T>)
OverloadTest(HashSet<? extends T>) 

At this point Java 8 employs "More Specific Method Inference", which analyses all pairs of candidates.

During checking if the former constructor is more specific than the latter, the following constraint is successfully resolved (T#0 is an inference variable for the type parameter T of HashSet):

OverloadTest<? extends T>  <: HashSet<? extends T#0>

The solution is to instantiate T#0 to List<T>.

The inverse attempt fails to resolve the following constraint (here T#0 is an inference variable for the type parameter T of OverloadTest):

HashSet<? extends T> <: OverloadTest<? extends T#0>

No instantiation for T#0 can be found to satisfy that constraint.

Since one direction succeeds and the other direction fails, the former constructor is considered more specific than the latter, thus resolving the ambiguity.

Ergo: accepting the program seems to be the correct answer from the compiler.

PS: I'm not arguing against more pedantic type checking in Java 8 where necessary, but this is not such a case.

Manipular answered 26/6, 2015 at 21:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.