13

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.

howlger
  • 31,050
  • 11
  • 59
  • 99
ernest_k
  • 44,416
  • 5
  • 53
  • 99
  • 3
    None of the three compile on Java 8. Now I'm not sure if this is a bug that got fixed in a newer version, or a bug/feature that got introduced... You should probably specify the Java version – Sweeper Apr 22 '20 at 10:08
  • @Sweeper I initially got this using jdk-13. Subsequent testing in java 8 (jdk8u232) shows the same errors. Not sure why the last one doesn't compile either on your machine. – ernest_k Apr 22 '20 at 10:22
  • Cannot reproduce on two online compilers either ([1](https://ideone.com/07WtCJ), [2](https://repl.it/repls/CuddlyEcstaticChord)). I am using 1.8.0_221 on my machine. This is getting weirder and weirder... – Sweeper Apr 22 '20 at 10:30
  • Do add the details of your compiler and/or IDE you are using. All three are ambiguous on the latest IntelliJ IDEA and JDK-14. Of course, `iterable.forEach((ConsumerOne) a -> aList.add(a));` and the other like, should compile for an explicit type-cast. – Naman Apr 22 '20 at 10:43
  • Thanks @ernest_k. I am listening here to get the answer – Gibbs Apr 22 '20 at 10:51
  • @Naman Added the details. I'm getting the error building in Eclipse (a clean maven build errors the last line too). Added details (and link to original question, which shows the same problem using different classes/types) – ernest_k Apr 22 '20 at 10:54
  • 1
    @ernest_k Eclipse has its own compiler implementation. That could be crucial information for the question. Also, the fact *a clean maven build errors the last line too* should be highlighted in the question in my opinion. On the other hand, about the linked question, the assumption that the OP is also using Eclipse could be clarified, since the code is not reproducible. – Naman Apr 22 '20 at 10:57
  • @Naman True. I've added the tag. I'm only testing those as you guys report that you can't reproduce. Will edit with these new findings. – ernest_k Apr 22 '20 at 10:59
  • Thanks @Holger, added eclipse version. – ernest_k Apr 22 '20 at 14:11
  • 3
    While I understand that intellectual value of the question, I can only advise against ever creating overloaded methods which only differ in functional interfaces and expecting that it can safely be called passing a lambda. I don't believe that the combination of lambda type inference and overloading is something that the average programmer will ever come close to understanding. It's an equation with very many variables that users don't control. AVOID THIS, please :) – Stephan Herrmann Apr 26 '20 at 21:49
  • @StephanHerrmann 100%. The scenario was found in some mongodb interfaces (if you see the linked question). I agree that one should avoid designing such a confusion. – ernest_k Apr 27 '20 at 05:19
  • @StephanHerrmann Yes, that's a Java puzzle that should be avoided in production code. Also because `javac` has a bug here, right (see [my answer below](https://stackoverflow.com/a/61442399/6505250))? Kudos to all of you ecj committers. – howlger Apr 27 '20 at 12:39

3 Answers3

8

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).

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Your assumption that the method call will be resolved to the inherited overloaded method is not correct. In fact, the method call will be resolved to the default method (see [my answer](https://stackoverflow.com/a/61442399/6505250)). – howlger Apr 26 '20 at 14:32
  • 1
    @howlger your comment makes no sense. The default method *is* the inherited method and the method is overloaded. There is no other method. The fact that the inherited method is a `default` method is just an additional point. My answer does already show an example where Eclipse does *not* give precedence to the default method. – Holger Apr 27 '20 at 06:58
  • It does not matter who inherits from whom. Your answer is about something that is irrelevant here and that nobody doubts (both compilers and the language specification agree on this). – howlger Apr 27 '20 at 07:09
  • [Nobody is interpreting nonsense into your answer](https://stackoverflow.com/conduct#unacceptable-behavior). Your answer says, _"There is no rule that would give precedence to an inherited applicable method."_ Sure. It does not matter who inherits from whom. That's just as irrelevant as the name of the methods or as the visibility of the class. The example in my anwer proves it: let `FooA extends FooAB` instead of `FooAB extends FooA` and watch what happens (spoiler: it makes no difference). – howlger Apr 27 '20 at 07:57
  • 1
    @howlger there is a fundamental difference in our behaviors. You only discovered that removing `default` changes the outcome and immediately assume to found the reason for the observed behavior. You are so overconfident about it that you call other answers wrong, despite the fact that they are not even contradicting. Because you are projecting your own behavior over other *I never claimed inheritance was the reason*. I proved that it’s not. I demonstrated that the behavior is inconsistent, as Eclipse selects the particular method in one scenario but not in another, where three overloads exist. – Holger Apr 27 '20 at 08:05
  • 1
    @howlger besides that, I already named another scenario at the end of [this comment](https://stackoverflow.com/questions/61361825/61368233?noredirect=1#comment108708742_61368233), create one interface, no inheritance, two methods, one `default` the other `abstract`, with two consumer like arguments and try it. Eclipse correctly says that it is ambiguous, despite one is a `default` method. Apparently inheritance still is relevant to this Eclipse bug, but unlike you, I’m not going mad and call other answers wrong, just because they didn’t analyze the bug to its entirety. That’s not our job here. – Holger Apr 27 '20 at 08:08
  • The point is that the default method is preferred here. Nothing else matters here or is something else that nobody doubts. Your answer is not wrong (I never said that), but is missing that point. – howlger Apr 27 '20 at 08:39
  • 1
    @howlger no, the point is that this is a bug. Most readers will not even care about the details. Eclipse could roll dice every time it selects a method, it doesn’t matter. Eclipse should not select a method when it’s ambiguous, so it doesn’t matter why it selects one. This answer proves that the behavior is inconsistent, which is already enough to strongly indicate that it’s a bug. It’s not necessary to point at the line in Eclipse’s source code where things go wrong. That’s not the purpose of Stackoverflow. Perhaps, you are confusing Stackoverflow with Eclipse’s bug tracker. – Holger Apr 27 '20 at 08:50
  • If it is an ecj bug, then for other reasons as claimed in your answer. What make you say _"You are so overconfident about it that you call other answers wrong"_ and _" Perhaps, you are confusing Stackoverflow with Eclipse’s bug tracker."_? – howlger Apr 27 '20 at 09:40
  • 1
    @howlger you are *again* incorrectly claiming that I made a (wrong) statement of why Eclipse made that wrong choice. Again, I didn’t, as eclipse shouldn’t make a choice at all. The method is ambiguous. Point. The reason why I used the term “*inherited*”, is because distinguishing identically named methods was necessary. I could have said “*default method*” instead, without changing the logic. More correctly, I should have used the phrase “*the very method Eclipse incorrectly selected for whatever reason*”. You can use either of the three phrases interchangeably, and the logic does not change. – Holger Apr 27 '20 at 09:52
  • So your answer to the question asking for the reason, is for whatever reason. Please note, my comments referred to your original answer, not to your [now changed answer with the stuff taken from my answer](https://stackoverflow.com/posts/61368233/revisions). You haven't answered yet, what make you say "You are so overconfident about it that you call other answers wrong" and " Perhaps, you are confusing Stackoverflow with Eclipse’s bug tracker."? What exactly triggers you to attack people you don't even know like this? – howlger Apr 27 '20 at 10:40
  • 1
    @howlger *Again*, my answer is **this is a bug**. I have provided an example demonstrating that *if* the behavior was for an existing rule, Eclipse was violating the hypothetical rule in another scenario. This demonstrates that either a) there is no such rule or b) there is a bug in the other scenario. You only discovered a tiny aspect, that `default` has an influence here. But your are behaving as if this was a prove that preferring `default` methods was a stressable rule rendering everything else irrelevant or even wrong (just read the comments from the beginning). Coincidence != Causality. – Holger Apr 27 '20 at 11:07
  • I disagree that this is an ecj bug since there is rule that give precedence to a default method when using explicitly typed lambda expressions (see my edited answer below). – howlger Apr 27 '20 at 12:47
  • 1
    @Lii it’s still some kind of wrong preference among ambiguous methods, so I’d say, it is related. – Holger May 04 '20 at 10:34
4

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.

Stephan Herrmann
  • 7,963
  • 2
  • 27
  • 38
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)
    }

}
howlger
  • 31,050
  • 11
  • 59
  • 99
  • Had also realized that typing the lambda explicitly was the solution for the caller. But I didn't realize that the `default` method made a difference... Thanks for pointing that out. – ernest_k Apr 27 '20 at 05:30
  • 4
    You missed the precondition right before the cited sentence: “*If all the maximally specific methods have override-equivalent signatures*” Of course, two methods whose arguments are entirely unrelated interfaces do not have override-equivalent signatures. Besides that, this wouldn’t explain why Eclipse stops selecting the `default` method when there are three candidate methods or when both methods are declared in the same interface. – Holger Apr 27 '20 at 14:21
  • 1
    @Holger Your answer claims, _"There is no rule that would give precedence to a particular applicable method when it is inherited or a default method."_ Do I understand you correctly that you are saying that the precondition for this non-existent rule does not apply here? Please note, the parameter here is a functional interface (see JLS 9.8). – howlger Apr 27 '20 at 15:46
  • 1
    You ripped a sentence out of context. The sentence describes the selection of *override-equivalent* methods, in other words, a choice between declarations which all would invoke the same method at runtime, because there will be only one concrete method in the concrete class. This is irrelevant to case of distinct methods like `forEach(Consumer)` and `forEach(Consumer2)` which can never end up at the same implementation method. – Holger Apr 27 '20 at 16:15
  • 1
    But I understand the confusion as with the wording of Java 8’s specification it would indeed require a clarification why this sentence does not apply here. Unfortunately, I didn’t look into this old specification and hence, did not realize how misleading it is. [Newer specifications](https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.12.2.5-530-A) correctly say “*concrete (that is, neither abstract nor default)*”, which would never create the impression that the sentence would apply here. Don’t know how much a reader could benefit from discussing this detail in an answer… – Holger Apr 27 '20 at 16:21
  • @Holger No, the resolution of overloaded methods is not done at runtime and the title of JLS 15.12.2 is _"Compile-Time Step 2: Determine Method Signature"_. Unfortunately, I don't have the time and desire to reply to comments that are neither nice nor add value (instead of _"You missed..."_, _"You ripped a sentence out of context."_, etc. [focus on the content, not the person](https://stackoverflow.com/conduct#unacceptable-behavior)). – howlger Apr 27 '20 at 17:21
  • 1
    A last attempt to explain: override-equivalent methods are different *declarations* from which the *compiler* must select one, but they end up at the same implementation at runtime. For example, invoking `equals(Object)` on a variable of type `` will have two candidates, one inherited from `Comparator` and one from `Object` (or `Foo` when it overrides it). The compiler has to select one to create bytecode, but whichever it selects, the actual (unknown at compile-time) implementation class will only have one method implementing both declarations. – Holger Apr 28 '20 at 07:36
  • @Holger as you mention the difference between JLS versions 8 and 14, do you have an idea, which JEP or JSR may have introduced these? I'm asking because that change has gone under our radar. OTOH it could be relevant for fixing Eclipse. To do so I would love to learn about the context and purpose of those changes. – Stephan Herrmann Apr 28 '20 at 22:01
  • 2
    @StephanHerrmann I don’t know a JEP or JSR, but the change looks like a fix to be in line with the meaning of “concrete”, i.e. compare with [JLS§9.4](https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.4-200): “*Default methods are distinct from concrete methods (§8.4.3.1), which are declared in classes.*”, which never changed. – Holger Apr 29 '20 at 07:44
  • 1
    @Holger, this makes sense, and I would have missed the difference still while looking at it, so thanks for pointing out. Initially I was, however, interested in what looked like a more substantial change around "A maximally specific method is _preferred_ if it has:". S.o. must have found a problem with the old phrase "the subset of the maximally specific methods that have the most specific return type." to feel the need to expand it into two new bullet lists. But then this change is probably irrelevant to the question at hand. – Stephan Herrmann Apr 30 '20 at 21:56
  • 2
    @StephanHerrmann yes, it looks like selecting a candidate from override-equivalent methods has become more complicated and it would be interesting to know the rationale behind it, but it is not relevant to the question at hand. There should be another document explaining changes and motivations. In the past, there was, but with this "a new version every ½ year" policy, keeping the quality seems impossible... – Holger May 01 '20 at 13:13