8

I have an overloaded method that takes two different functional interfaces as parameters (Runnble and Supplier). System.out.println is clearly only compatible with Runnable, because it is a void method. Yet the compiler still claims that the call is ambiguous. How is that possible?

import java.util.function.Supplier;

public class GenericLambdas {
    public static void main(String[] args) {
        wrap(System.out::println);   // Compiler error here
        wrap(() -> {});              // No error
        wrap(System.out::flush);     // No error
    }

    static <R> void wrap(Supplier<R> function) {}

    static void wrap(Runnable function) {}
}

Compiler output:

Error:Error:line (5)java: reference to wrap is ambiguous
    both method <R>wrap(java.util.function.Supplier<R>) in GenericLambdas and method wrap(java.lang.Runnable) in GenericLambdas match 
Error:Error:line (5)java: incompatible types: cannot infer type-variable(s) R
    (argument mismatch; bad return type in method reference
      void cannot be converted to R)

Based on the second error (argument mismatch, void cannot be converted to R), shouldn't the compiler be able to disambiguate the call? That would then take care of both compiler errors (as it will neither be ambiguous, nor will it try to convert void to R).

And why are () -> {} and System.out::flush able to resolve? They have the same signature as System.out.println. Granted that System.out.println is overloaded with versions that take an argument, but none of those overloaded versions match either Supplier or Runnable, so I don't see how they would be relevant here.

EDIT:

It seems that this does compile and run with the Eclipse compiler. Which compiler is correct, or is either behavior allowed?

Community
  • 1
  • 1
Yosef Weiner
  • 5,432
  • 1
  • 24
  • 37
  • It does compile fine when I copy paste the code. – Yassin Hajaj Oct 14 '15 at 20:06
  • 3
    As a general rule, overloads with multiple functional interfaces are almost always a bad idea. Just use a different method name. – Louis Wasserman Oct 14 '15 at 20:11
  • 1
    Reproduced in command-line `javac` from JDK 1.8.0_60. In Eclipse it compiles without ambiguity. – RealSkeptic Oct 14 '15 at 20:11
  • @YassinHajaj what compiler are you using? – Yosef Weiner Oct 14 '15 at 20:14
  • SE-1.8 I guess. @SkinnyJ – Yassin Hajaj Oct 14 '15 at 20:14
  • 1
    @YassinHajaj `javac` from command line or inside an IDE? – RealSkeptic Oct 14 '15 at 20:16
  • Inside the IDE, it doesn't give me an error message and executes also. I'm on Eclipse and using Java 8 and the compiler is Java SE 1.8 – Yassin Hajaj Oct 14 '15 at 20:16
  • http://imgur.com/VaSAPDt Here is a printscreen of Eclipse @SkinnyJ ... What compiler do you see? – Yassin Hajaj Oct 14 '15 at 20:21
  • @YassinHajaj tried it with Oracle JDK 1.8.0_40 and 1.8.0_60, got the error both ways. – Yosef Weiner Oct 14 '15 at 20:23
  • @SkinnyJ As you see, it does compile fine for me. Tell me what info you need and I'll provide it. – Yassin Hajaj Oct 14 '15 at 20:24
  • I just confirmed that [changing the compiler in IntelliJ to the Eclipse compiler](https://confluence.jetbrains.com/display/IntelliJIDEA/Compiler) allows it to compile and run. I wonder which is correct. I will edit the question. – Yosef Weiner Oct 14 '15 at 20:34
  • 2
    Another interesting point: if you substitute `System.out::flush` instead of `println`, the error is gone. So the issue stems from the fact that `println` is overloaded. – RealSkeptic Oct 14 '15 at 20:52
  • @RealSkeptic I had a suspicion when I saw that the anonymous lambda compiled. But how would the overloaded versions have any relevance to `Runnable` or `Supplier`, neither of which take any parameters? – Yosef Weiner Oct 14 '15 at 21:29
  • 1
    The compiler has to figure out which version of `println` is to be used and the return type obviously plays no role here. I think that's according to the specification, but I'm not entirely sure. Keep in mind that the return type isn't part of a method signature either. So from that perspective both `Supplier#get` and `Runnable#run` would match. – a better oliver Oct 14 '15 at 21:36
  • 3
    This **might** be a not-so-obvious duplicate of http://stackoverflow.com/questions/29323520/reference-to-method-is-ambiguous-when-using-lambdas-and-generics ... – Marco13 Oct 14 '15 at 22:02
  • 1
    @Marco13 I think you're right http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8077243 – Yosef Weiner Oct 28 '15 at 21:06

2 Answers2

4

Finding the appropriate version of println ignores the return type. See JLS 15.13.2. It wouldn't make sense to include it, because there can't be two versions of a method with the same parameters but a different return type. But now the compiler's got a problem: Both Supplier#get and Runnable#run expect the same parameters (none). So there's a println that would match for both. Keep in mind that in this stage, the compiler only tries to find a method. The compiler basically runs into the same problem as in this code:

public static void main(String[] args) {
 test(System.out::println);
}

public static void test(Runnable r) {}
public static void test(Consumer<String> r) {}

println() matches Runnable#run and println(String) matches Consumer#accept. We didn't provide a target type, so the situation is ambiguous.

After a method has been chosen, the target type can be inferred correctly, and in this stage the return type is relevant. See JLS 15.13.2. So this code would fail, of course:

public static void main(String[] args) {
    wrap(System.out::println);
}

static <R> void wrap(Supplier<R> function) {}

The compiler immediately throws an error when it detects the ambiguity. Although a bug report has been raised and accepted for this behavior, the comments there indicate that Oracle's JDK may be adhering to the JLS more faithfully than ECJ (despite its nicer behavior). A later bug report, raised on the back of this SO question, was resolved as "Not an issue", indicating that, after internal debate, Oracle has decided that javac's behavior is correct.

Yosef Weiner
  • 5,432
  • 1
  • 24
  • 37
a better oliver
  • 26,330
  • 2
  • 58
  • 66
  • Wouldn't the same apply to `System.out::flush`? However, that one selects the correct (`Runnable`) overload. – Yosef Weiner Oct 15 '15 at 13:22
  • 1
    @SkinnyJ I assume that if there is only one method with that identifier then no search is necessary. So the process described in JLS 15.13.1. wouldn't apply and the compiler can go straight into inferring the target type. – a better oliver Oct 15 '15 at 13:52
  • It's also strange that the compiler outputs both errors. Based on the way you describe it above, I would think that once it decides `println` is ambiguous, it would not also complain about the return type. If it's complaining about the bad return type, that means it has already matched `println` to `Supplier`. – Yosef Weiner Oct 15 '15 at 17:24
  • 1
    @SkinnyJ The compiler hasn't decided to use the `Supplier` version, but it wants to create a typed version of ` wrap`. That could be a bug. On the other hand both errors occur at the same line. Change the order of the `wrap` methods and the second error is gone. – a better oliver Oct 15 '15 at 17:47
  • fascinating. That certainly seems strange. So at this point you are saying that the ambiguity error is a limitation (possible bug) because of the rules for overloaded methods being checked before the rules for functional interface inference, and the return type error is possibly a bug, but not so relevant because it would not on its own prevent compilation? That sounds reasonable. – Yosef Weiner Oct 15 '15 at 18:07
  • 1
    @SkinnyJ: that’s not a new feature. The compiler doesn’t stop on the first error and in the course of reporting additional errors, it doesn’t recognize which are follow-up errors. You can easily get twenty errors by just omitting a single brace or comma (or by inserting it at the wrong place)… – Holger Oct 15 '15 at 18:49
  • @Holger I was fascinated by the fact that reversing the order of the method definitions caused the error to disappear. – Yosef Weiner Oct 15 '15 at 19:01
  • Looking back at this a couple weeks later, I think that @Marco13 's comment and this bug report are exactly right (which is basically what you explained). From the notes in the bug report, it looks like even the Oracle engineers can't agree if this is a bug or accurately following the JLS. http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8077243 – Yosef Weiner Oct 28 '15 at 21:21
2

It seems like a bug in javac compiler. The problem lies in overloaded println() method. It has different implementations based on type you write: int, long, String, etc. So expression: System.out::println has 10 methods to choose from. One of them can be deduced to Runnable, 9 others to Consumer<T>.

Somehow, javac compiler is not able to infer correct method implementation from this expression. And wrap(() -> {}) compiles correctly because this expression has only one possible interpretation – Runnable.

I'm not sure is it allowed to have such expressions under JLS rules. But following code compiles correctly using javac (and runs without runtime issues):

wrap((Runnable)System.out::println);

It seems like this cast provide required information to the compiler to deduce type correctly, which is kind of strange. I didn't know cast expressions can be used in type inference.

Denis Bazhenov
  • 9,680
  • 8
  • 43
  • 65
  • Submitted a bug report to Oracle. Let's see what they say. – Yosef Weiner Oct 15 '15 at 03:53
  • 1
    @SkinnyJ, I'm not 100% sure, but probably it's expected behavior. When [JLS](https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.13.1) describes method reference resolution it says about arguments matching, but nothing about return type matching. Thus it's likely that when there's an ambiguity on arguments a compiler error *must* occur even if return type may disambiguate this. – Tagir Valeev Oct 15 '15 at 11:00
  • @TagirValeev in that case, why does `() -> {}` resolve OK? If you ignore the return type, that would match both `Runnable` and `Supplier` as well. – Yosef Weiner Oct 15 '15 at 11:12
  • @SkinnyJ, because lambda is not method reference and you don't need to resolve lambda expression as there's one exact expression specified. There's value-compatible and void-compatible lambdas and their return type matters during the outer method call resolution, see [here](https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.27.2). – Tagir Valeev Oct 15 '15 at 11:26
  • @TagirValeev what about `System.out::flush`? – Yosef Weiner Oct 15 '15 at 11:34
  • @SkinnyJ: that’s explained in the question, [Marco13 has linked to](http://stackoverflow.com/questions/33134456/compiler-not-inferring-system-outprintln-functional-interface#comment54084287_33134456). The problem with `wrap(…::println)` is that the compiler needs a target type to find out which `println` method to use but can’t decide because it doesn’t know which `wrap` method to use. In case of `flush` there is no such problem as there is only one `flush` method. One method means no ambiguity, so the functional signature is known and can be used to select an appropriate `wrap` method. – Holger Oct 15 '15 at 13:49
  • @TagirValeev no it won't. Java compiler doesn't ignore return type wile matching method signatures, as well as doesn't do conversion between `void` and `java.lang.Void` types. This is why `() -> {}` has only one interpretation. You can easily check it for yourself: `Supplier a = () -> {};`. As you correctly pointed out, those are two distinct types of lambdas: void-compatible and value-compatible. – Denis Bazhenov Oct 16 '15 at 00:28
  • FYI [here](http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8144169) the bug report that I submitted – Yosef Weiner Dec 09 '15 at 15:02