47

Bear with me, the introduction is a bit long-winded but this is an interesting puzzle.

I have this code:

public class Testcase {
    public static void main(String[] args){
        EventQueue queue = new EventQueue();
        queue.add(() -> System.out.println("case1"));
        queue.add(() -> {
            System.out.println("case2");
            throw new IllegalArgumentException("case2-exception");});
        queue.runNextTask();
        queue.add(() -> System.out.println("case3-never-runs"));
    }

    private static class EventQueue {
        private final Queue<Supplier<CompletionStage<Void>>> queue = new ConcurrentLinkedQueue<>();

        public void add(Runnable task) {
            queue.add(() -> CompletableFuture.runAsync(task));
        }

        public void add(Supplier<CompletionStage<Void>> task) {
            queue.add(task);
        }

        public void runNextTask() {
            Supplier<CompletionStage<Void>> task = queue.poll();
            if (task == null)
                return;
            try {
                task.get().
                    whenCompleteAsync((value, exception) -> runNextTask()).
                    exceptionally(exception -> {
                        exception.printStackTrace();
                        return null; });
            }
            catch (Throwable exception) {
                System.err.println("This should never happen...");
                exception.printStackTrace(); }
        }
    }
}

I am trying to add tasks onto a queue and run them in order. I was expecting all 3 cases to invoke the add(Runnable) method; however, what actually happens is that case 2 gets interpreted as a Supplier<CompletionStage<Void>> that throws an exception before returning a CompletionStage so the "this should never happen" code block gets triggered and case 3 never runs.

I confirmed that case 2 is invoking the wrong method by stepping through the code using a debugger.

Why isn't the Runnable method getting invoked for the second case?

Apparently this issue only occurs on Java 10 or higher, so be sure to test under this environment.

UPDATE: According to JLS §15.12.2.1. Identify Potentially Applicable Methods and more specifically JLS §15.27.2. Lambda Body it seems that () -> { throw new RuntimeException(); } falls under the category of both "void-compatible" and "value-compatible". So clearly there is some ambiguity in this case but I certainly don't understand why Supplier is any more appropriate of an overload than Runnable here. It's not as if the former throws any exceptions that the latter does not.

I don't understand enough about the specification to say what should happen in this case.

I filed a bug report which is visible at https://bugs.openjdk.java.net/browse/JDK-8208490

Martin Schröder
  • 4,176
  • 7
  • 47
  • 81
Gili
  • 86,244
  • 97
  • 390
  • 689
  • Did something happen when you pasted your code? `queue.add(queue.add(() -> CompletableFuture.runAsync(task));` not only doesn't make sense, it doesn't compile. The parentheses don't even match up. – David Conrad Jul 29 '18 at 05:10
  • Do you say that the first call to add() hits the first overloading and the second call the second overloading because you put breakpoints on both overloadings? Or are you deducing that is what happened, because of that later behavior when runNextTask() is invoked? – moilejter Jul 29 '18 at 05:18
  • @John It does for me, on Java 11. Apparently the compiler makes that decision based on the presence of the `throw` statement, because if you comment it out, they both go to the one that takes `Runnable`. – David Conrad Jul 29 '18 at 05:25
  • If, on the other hand, the `throw` is inside an `if`, it doesn't. Strange. – David Conrad Jul 29 '18 at 05:27
  • @DavidConrad Good catch. I've updated the question – Gili Jul 29 '18 at 05:27
  • @HovercraftFullOfEels You are right. The question has been updated with a running testcase. Thank you for nudging me in the right direction. – Gili Jul 29 '18 at 05:28
  • It seems like it should be possible to get a ClassCastException at runtime, except the fact that `get()` throws prevents us from getting there, and any change I make to avoid throwing the exception causes the compiler to switch back to the correct type. I've reproduced it using just `Supplier`, there's no need to involve concurrent anything. This definitely looks like a compiler bug. – David Conrad Jul 29 '18 at 05:52
  • 3
    *Apparently this issue only occurs on Java 10 or higher, so be sure to test under this environment*... I get the same behavior in `Java-8`, are you sure about the tag? – Naman Jul 29 '18 at 14:50
  • 2
    Here is the [link to reproduced sample with Java-8.](https://tio.run/##y0osS9TNL0jNy0rJ/v@/oDQpJzNZITknsbhYwTcxM0@hmosTKlhcklgCpMryM1MUcoFSGsElRZl56dGxColF6cWaIJWcwZXFJam5evmlJXoFQMmSnDwNJY/UnJx8HYXw/KKcFEUlTWsuzlqu2v//AQ). Just in case, I am missing out something, please do point out. – Naman Jul 29 '18 at 15:04
  • 4
    This is interesting, but I don't share the intuition that the `Runnable` overload should be chosen. It seems to me that e.g. `() -> { throw new RuntimeException(); }` ought to be compatible with a value-returning function type, and if it's compatible, then which overload is actually chosen could be arbitrary. (And because of that, I doubt this is a bug, even if it might not be intuitive to some.) Passing an exception-throwing lambda to e.g. a function or supplier is occasionally useful (esp. for tests), and if it wasn't compatible we'd have to use some pretty silly workarounds. – Radiodef Jul 29 '18 at 16:23
  • 1
    @nullpointer You are right the issue is reproducible in Java 8 and up. Thanks for the head's up. – Gili Jul 30 '18 at 14:16
  • 4
    The exceptions are a red herring, as you could use `() -> { for(;;); }` to get the same result. – Holger Jul 30 '18 at 14:59
  • The [ticket](https://bugs.openjdk.java.net/browse/JDK-8208490) resolved as "Not an Issue". – Oleksandr Pyrohov Aug 25 '18 at 12:16

5 Answers5

20

The problem is that there are two methods:

void fun(Runnable r) and void fun(Supplier<Void> s).

And an expression fun(() -> { throw new RuntimeException(); }).

Which method will be invoked?

According to JLS §15.12.2.1, the lambda body is both void-compatible and value-compatible:

If the function type of T has a void return, then the lambda body is either a statement expression (§14.8) or a void-compatible block (§15.27.2).

If the function type of T has a (non-void) return type, then the lambda body is either an expression or a value-compatible block (§15.27.2).

So both methods are applicable to the lambda expression.

But there are two methods so java compiler needs to find out which method is more specific

In JLS §15.12.2.5. It says:

A functional interface type S is more specific than a functional interface type T for an expression e if all of the following are true:

One of the following is:

Let RS be the return type of MTS, adapted to the type parameters of MTT, and let RT be the return type of MTT. One of the following must be true:

One of the following is:

RT is void.

So S (i.e. Supplier) is more specific than T (i.e. Runnable) because the return type of the method in Runnable is void.

So the compiler choose Supplier instead of Runnable.

Community
  • 1
  • 1
zhh
  • 2,346
  • 1
  • 11
  • 22
  • 1
    This doesn't explain why the first lambda does not match `Supplier`. By the same logic, shouldn't it also do so? – Gili Jul 29 '18 at 06:57
  • 1
    @Gill The first lambda is only void-compatible. – zhh Jul 29 '18 at 06:58
  • @Gill First of all, the lambda must be applicable to both method, only the one which is both void-competible and value-compatible satisfy this criteria. – zhh Jul 29 '18 at 06:59
  • 1
    Maybe I missed something but the paragraph you referenced does not say that it only applies to lambdas that are both void-compatible and value-compatible. – Gili Jul 29 '18 at 07:00
  • @Gill This does not in this section, you can find it here. https://docs.oracle.com/javase/specs/jls/se10/html/jls-15.html#jls-15.12.2.1 – zhh Jul 29 '18 at 07:03
  • 1
    I think you might be on to something though boy this is unintuitive :) We'll see what the guys at Oracle say (I filed a formal bug report). – Gili Jul 29 '18 at 07:07
  • I agree this is a good find, and it helped me find an even simpler example which I've updated my answer with. I need more time to digest this mess of MT's and RT's and RS's. – David Conrad Jul 29 '18 at 07:30
  • @Gili I have updated my answer. Hope it is easier to understand. – zhh Jul 29 '18 at 07:54
  • I've updated the question with a link to the bug report. – Gili Jul 30 '18 at 14:16
11

First, according to §15.27.2 the expression:

() -> { throw ... }

Is both void-compatible, and value-compatible, so it's compatible (§15.27.3) with Supplier<CompletionStage<Void>>:

class Test {
  void foo(Supplier<CompletionStage<Void>> bar) {
    throw new RuntimeException();
  }
  void qux() {
    foo(() -> { throw new IllegalArgumentException(); });
  }
}

(see that it compiles)

Second, according to §15.12.2.5 Supplier<T> (where T is a reference type) is more specific than Runnable:

Let:

  • S := Supplier<T>
  • T := Runnable
  • e := () -> { throw ... }

So that:

  • MTs := T get() ==> Rs := T
  • MTt := void run() ==> Rt := void

And:

  • S is not a superinterface or a subinterface of T
  • MTs and MTt have the same type parameters (none)
  • No formal parameters so bullet 3 is also true
  • e is an explicitly typed lambda expression and Rt is void
duvduv
  • 649
  • 6
  • 10
  • 2
    I find your answer easier to understand than [zhh's](https://stackoverflow.com/a/51577984/14731) but you seemed to have missed a key point he mentioned: §15.12.2.5 says that Rs is more specific than Rt if Rt is `void`. Hence, `Supplier` is more specific than `Runnable`. – Gili Jul 29 '18 at 07:10
  • Thanks @Gili, I thought my result was a bit strange. I've updated the answer. – duvduv Jul 29 '18 at 12:26
  • AH! Now I get it. Thank you. 15.27.2 is the key point, where it says that in a lambda that cannot complete normally, every `return` must be of the form return *Expression*, which is trivially true if there are zero returns. I would have added, and there must be at least one return *Expression*, but they clearly wanted to allow value lambdas that can only return abnormally. Something I confess doesn't seem very useful to me. – David Conrad Jul 29 '18 at 15:59
7

It appears that when throwing an Exception, the compiler chooses the interface which returns a reference.

interface Calls {
    void add(Runnable run);

    void add(IntSupplier supplier);
}

// Ambiguous call
calls.add(() -> {
        System.out.println("hi");
        throw new IllegalArgumentException();
    });

However

interface Calls {
    void add(Runnable run);

    void add(IntSupplier supplier);

    void add(Supplier<Integer> supplier);
}

complains

Error:(24, 14) java: reference to add is ambiguous both method add(java.util.function.IntSupplier) in Main.Calls and method add(java.util.function.Supplier) in Main.Calls match

Lastly

interface Calls {
    void add(Runnable run);

    void add(Supplier<Integer> supplier);
}

compiles fine.

So weirdly;

  • void vs int is ambiguous
  • int vs Integer is ambiguous
  • void vs Integer is NOT ambiguous.

So I figure something is broken here.

I have sent a bug report to oracle.

Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • I don't understand your last sentence "the second lambda has to return a reference. Returning a void or primitive leaves the calls ambiguous." If it was ambiguous, wouldn't the compiler throw an error? https://stackoverflow.com/q/23430854/14731 implies it would. – Gili Jul 29 '18 at 06:23
  • @Gili I used a different interface to either return void or return an `int` and these were seen as ambiguous. – Peter Lawrey Jul 29 '18 at 06:24
  • 1
    There is no ambiguity between `void` and `int` returning functions, unless you use a very old compiler (Java 8, before update 20). All `javac` versions from 8u20 to 10 prefer the non-`void` function. And even the old compilers show consistent behavior regarding `int` and `Integer`, reporting an ambiguity in either case. But the beginning of your error message, “Error:(24, 14) java:” doesn’t look like `javac` output anyway… – Holger Jul 30 '18 at 11:54
5

First things first:

The key point is that overloading methods or constructors with different functional interfaces in the same argument position causes confusion. Therefore, do not overload methods to take different functional interfaces in the same argument position.

Joshua Bloch, - Effective Java.

Otherwise, you'll need a cast to indicate the correct overloading:

queue.add((Runnable) () -> { throw new IllegalArgumentException(); });
              ^

The same behavior is evident when using an infinite loop instead of a runtime exception:

queue.add(() -> { for (;;); });

In the cases shown above, the lambda body never completes normally, which adds to the confusion: which overload to choose (void-compatible or value-compatible) if the lambda is implicitly typed? Because in this situation both methods become applicable, for example you can write:

queue.add((Runnable) () -> { throw new IllegalArgumentException(); });

queue.add((Supplier<CompletionStage<Void>>) () -> {
    throw new IllegalArgumentException();
});

void add(Runnable task) { ... }
void add(Supplier<CompletionStage<Void>> task) { ... }

And, like stated in this answer - the most specific method is chosen in case of ambiguity:

queue.add(() -> { throw new IllegalArgumentException(); });
                       ↓
void add(Supplier<CompletionStage<Void>> task);

At the same time, when the lambda body completes normally (and is void-compatible only):

queue.add(() -> { for (int i = 0; i < 2; i++); });
queue.add(() -> System.out.println());

the method void add(Runnable task) is chosen, because there is no ambiguity in this case.

As stated in the JLS §15.12.2.1, when a lambda body is both void-compatible and value-compatible, the definition of potential applicability goes beyond a basic arity check to also take into  account  the presence and shape of functional interface target types.

Oleksandr Pyrohov
  • 14,685
  • 6
  • 61
  • 90
  • 2
    The quote, your first thing, should be enough to settle this case. Disregarding that advice is asking for trouble. Even experts have difficulties figuring out which overload will be chosen. @Gili have mercy with readers of that program -- unless your goal is to create a quiz for the top 0.1% experts. – Stephan Herrmann Jul 29 '18 at 21:32
  • Where edition and item number is your Effective Java quote from? I bought the 3rd edition but haven't had a chance to read it yet. I guess now I should make the time :) – Gili Jul 30 '18 at 03:38
  • 2
    @Gili Chapter 8, item 52: Use overloading judiciously. – Oleksandr Pyrohov Jul 30 '18 at 06:24
  • 3
    @StephanHerrmann and regardless of functional interfaces, overloaded methods should not do fundamentally different things as in that question’s code which wraps the argument into `CompletableFuture.runAsync` or enqueues it directly. To name a positive example, the `ExecutorService` does it right, both, `submit(Callable)` and `submit(Runnable)`, wrap the function into a future, whereas `execute(Runnable)` is the method dedicated to enqueue the `Runnable` directly without wrapping. So calling the “wrong” `submit` method would never have a negative effect then. – Holger Jul 30 '18 at 12:11
  • 1
    Thank you for the quote from Josh Bloch, it's most illuminating. – David Conrad Jul 30 '18 at 19:54
  • 3
    @Holger I agree that a dangerous feature applied with great discipline will be less dangerous. Still I think Joshua Bloch was very modest in that quoted piece of advice. For a more drastic warning against overloading I recommend https://gbracha.blogspot.com/2009/09/systemic-overload.html - already before Java 8 overloading could be used for quite obscure programming. – Stephan Herrmann Jul 31 '18 at 18:30
2

I wrongly considered this a bug, but it appears to be correct according to §15.27.2. Consider:

import java.util.function.Supplier;

public class Bug {
    public static void method(Runnable runnable) { }

    public static void method(Supplier<Integer> supplier) { }

    public static void main(String[] args) {
        method(() -> System.out.println());
        method(() -> { throw new RuntimeException(); });
    }
}
javac Bug.java
javap -c Bug
public static void main(java.lang.String[]);
  Code:
     0: invokedynamic #2,  0      // InvokeDynamic #0:run:()Ljava/lang/Runnable;
     5: invokestatic  #3          // Method add:(Ljava/lang/Runnable;)V
     8: invokedynamic #4,  0      // InvokeDynamic #1:get:()Ljava/util/function/Supplier;
    13: invokestatic  #5          // Method add:(Ljava/util/function/Supplier;)V
    16: return

This happens with jdk-11-ea+24, jdk-10.0.1, and jdk1.8u181.

zhh's answer led me to find this even simpler test case:

import java.util.function.Supplier;

public class Simpler {
    public static void main(String[] args) {
        Supplier<Integer> s = () -> { throw new RuntimeException(); };
    }
}

However, duvduv pointed out §15.27.2, in particular, this rule:

A block lambda body is value-compatible if it cannot complete normally (§14.21) and every return statement in the block has the form return Expression;.

Thus, a block lambda is trivially value-compatible even if it contains no return statement at all. I would have thought, because the compiler needs to infer its type, that it would require at least one return Expression;. Holgar and others have pointed out that this is not necessary with ordinary methods such as:

int foo() { for(;;); }

But in that case the compiler only needs to ensure there is no return that contradicts the explicit return type; it doesn't need to infer a type. However, the rule in the JLS is written to allow the same freedom with block lambdas as with ordinary methods. Perhaps I should have seen that sooner, but I did not.

I filed a bug with Oracle but have since sent an update to it referencing §15.27.2 and stating that I believe my original report to be in error.

David Conrad
  • 15,432
  • 2
  • 42
  • 54
  • 2
    why do you call it a bug? – Andrew Tobilko Jul 29 '18 at 06:10
  • I've updated the question with a reference to the Java specification. See if you can understand what should happen in this case. The document is a bit too technical for me ;) – Gili Jul 29 '18 at 06:13
  • 1
    @Andrew I call it a bug because there's no way the compiler should ever consider a lambda that doesn't return anything as being a `Supplier` of anything. It has clearly deduced the wrong type, here. – David Conrad Jul 29 '18 at 06:18
  • I hope you don't mind, I will use your minimal testcase to file a bug report. It is more concise than my own. I will post the bug report link once it's public. – Gili Jul 29 '18 at 06:30
  • @Gili Ah, I did just file one (and linked back here and referenced you as the original discoverer). – David Conrad Jul 29 '18 at 06:50
  • 1
    @DavidConrad Oops. Okay, well now they have two reports. I also linked back here. Be sure to link to your bug report once it becomes public. Thanks in advance. – Gili Jul 29 '18 at 06:54
  • @Gili No harm, I'm sure. I will link to it, as you say. Cheers! – David Conrad Jul 29 '18 at 06:59
  • I agree with the first part of your answer but I think your "simpler test case" is wrong. There is nothing wrong with `{ throws new RuntimeException(); }` mapping to a `Supplier` or `Runnable`. Both are allowed to throw runtime exceptions. The only problem I see is the lambda interface changing depending on whether the implementation throws an exception or not. – Gili Jul 29 '18 at 07:38
  • @Gili I guess I just don't see how something that returns void can ever match a Supplier, unless it was a `Supplier` or something (??), but it's late. I'm going to read through what zhh provided in the morning. – David Conrad Jul 29 '18 at 07:44
  • 2
    @DavidConrad following the logic that 'there's no way the compiler should ever consider a lambda that doesn't return anything as being a Supplier of anything', `public Integer getInteger() { throw new RuntimeException();}` (as well as `Supplier s = this::getInteger`) should be illegal as well – crizzis Jul 29 '18 at 11:08
  • @crizzis That's a good point, but in that case the compiler doesn't need to infer the type or choose between overloads. – David Conrad Jul 29 '18 at 15:46
  • 3
    FWIW: javac and ecj agree on how to handle Bug.java. This could indicate that this behavior is mandated by JLS and hence you would not be challenging javac but JLS, i.e., you expect a different language than what JLS defines. – Stephan Herrmann Jul 29 '18 at 21:22
  • @Stephan Thanks. Yes, per duvduv's answer it turns out the compilers are correct and my quarrel is with the JLS. – David Conrad Jul 29 '18 at 21:42
  • 2
    You’re challenging a very old rule, considering that `int foo() { for(;;); }` always has been a valid method definition… – Holger Jul 30 '18 at 11:30
  • @Holger I wouldn't say "challenging" so much as "misunderstanding." I had thought the rules would be different for lambdas where type inference is involved (as I state in my update, which see), but they are not. I am now convinced that this is not a bug and I was originally mistaken. – David Conrad Jul 30 '18 at 19:50