Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ambiguous overload in Java8 - is ECJ or javac right?

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.

like image 429
Ian Robertson Avatar asked Oct 06 '14 18:10

Ian Robertson


1 Answers

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.

like image 141
Steve K Avatar answered Oct 17 '22 22:10

Steve K