Method signature selection for lambda expression w

2020-07-03 04:52发布

问题:

I was answering a question and ran into a scenario I can't explain. Consider this code:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

I do not understand why explicitly typing the paramter of the lambda (A a) -> aList.add(a) makes the code compile. Additionally, why does it link to the overload in Iterable rather than the one in CustomIterable?
Is there some explanation to this or a link to the relevant section of the spec?

Note: iterable.forEach((A a) -> aList.add(a)); only compiles when CustomIterable<T> extends Iterable<T> (flatly overloading the methods in CustomIterable results in the ambiguous error)


Getting this on both:

  • openjdk version "13.0.2" 2020-01-14
    Eclipse compiler
  • openjdk version "1.8.0_232"
    Eclipse compiler

Edit: The code above fails to compile on building with maven while Eclipse compiles the last line of code successfully.

回答1:

TL;DR, this is a compiler bug.

There is no rule that would give precedence to a particular applicable method when it is inherited or a default method. Interestingly, when I change the code to

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

the iterable.forEach((A a) -> aList.add(a)); statement produces an error in Eclipse.

Since no property of the forEach(Consumer<? super T) c) method from the Iterable<T> interface changed when declaring another overload, Eclipse’s decision to select this method can not be (consistently) based on any property of the method. It’s still the only inherited method, still the only default method, still the only JDK method, and so on. Neither of these properties should affect the method selection anyway.

Note that changing the declaration to

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

also produces an “ambiguous” error, so the number of applicable overloaded methods doesn’t matter either, even when there are only two candidates, there is no general preference towards default methods.

So far, the issue seems to appear when there are two applicable methods and a default method and an inheritance relationship are involved, but this is not the right place to dig further.


But it’s understandable that the constructs of your example may be handled by different implementation code in the compiler, one exhibiting a bug while the other doesn’t.
a -> aList.add(a) is an implicitly typed lambda expression, which can’t be used for the overload resolution. In contrast, (A a) -> aList.add(a) is an explicitly typed lambda expression which can be used to select a matching method from the overloaded methods, but it doesn’t help here (shouldn’t help here), as all methods have parameter types with exactly the same functional signature.

As a counter-example, with

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

the functional signatures differ, and using an explicitly type lambda expression can indeed help selecting the right method whereas the implicitly typed lambda expression doesn’t help, so forEach(s -> s.isEmpty()) produces a compiler error. And all Java compilers agree about that.

Note that aList::add is an ambiguous method reference, as the add method is overloaded too, so it also can’t help selecting a method, but method references might get processed by different code anyway. Switching to an unambiguous aList::contains or changing List to Collection, to make add unambiguous, did not change the outcome in my Eclipse installation (I used 2019-06).



回答2:

The code where Eclipse implements JLS §15.12.2.5 does not find either method as more specific than the other, even for the case of the explicitly typed lambda.

So ideally Eclipse would stop here and report ambiguity. Unfortunately, the implementation of overload resolution has non-trivial code in addition to implementing JLS. From my understanding this code (which dates from the time when Java 5 was new) must be kept to fill in some gaps in JLS.

I've filed https://bugs.eclipse.org/562538 to track this.

Independent of this particular bug, I can only strongly advise against this style of code. Overloading is good for a good number of surprises in Java, multiplied with lambda type inference, complexity is quite out of proportion wrt the perceived gain.



回答3:

The Eclipse compiler correctly resolves to the default method, since this is the most specific method according to the Java Language Specification 15.12.2.5:

If exactly one of the maximally specific methods is concrete (that is, non-abstract or default), it is the most specific method.

javac (used by Maven and IntelliJ by default) tells the method call is ambiguous here. But according to Java Language Specification it is not ambiguous since one of the two methods is the most specific method here.

Implicitly typed lambda expressions are handled differently than explicitly typed lambda expressions in Java. Implicitly typed in contrast to explicitly typed lambda expressions fall through the first phase to identify the methods of strict invocation (see Java Language Specification jls-15.12.2.2, first point). Hence, the method call here is ambiguous for implicitly typed lambda expressions.

In your case, the workaround for this javac bug is to specify the type of the functional interface instead of using an explicitly typed lambda expression as follows:

iterable.forEach((ConsumerOne<A>) aList::add);

or

iterable.forEach((Consumer<A>) aList::add);

Here is your example further minimized for testing:

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}