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
I wrongly considered this a bug, but it appears to be correct according to §15.27.2. Consider:
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:
However, duvduv pointed out §15.27.2, in particular, this rule:
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:
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.
First, according to §15.27.2 the expression:
Is both
void
-compatible, and value-compatible, so it's compatible (§15.27.3) withSupplier<CompletionStage<Void>>
:(see that it compiles)
Second, according to §15.12.2.5
Supplier<T>
(whereT
is a reference type) is more specific thanRunnable
:Let:
Supplier<T>
Runnable
() -> { throw ... }
So that:
T get()
==> Rs :=T
void run()
==> Rt :=void
And:
S
is not a superinterface or a subinterface ofT
void
It appears that when throwing an Exception, the compiler chooses the interface which returns a reference.
However
complains
Lastly
compiles fine.
So weirdly;
void
vsint
is ambiguousint
vsInteger
is ambiguousvoid
vsInteger
is NOT ambiguous.So I figure something is broken here.
I have sent a bug report to oracle.
The problem is that there are two methods:
void fun(Runnable r)
andvoid 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:
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:
One of the following is:
One of the following is:
So S (i.e.
Supplier
) is more specific than T (i.e.Runnable
) because the return type of the method inRunnable
isvoid
.So the compiler choose
Supplier
instead ofRunnable
.First things first:
Otherwise, you'll need a cast to indicate the correct overloading:
The same behavior is evident when using an infinite loop instead of a runtime exception:
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:
And, like stated in this answer - the most specific method is chosen in case of ambiguity:
At the same time, when the lambda body completes normally (and is void-compatible only):
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.