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