Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a lambda change overloads when it throws a runtime exception?

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

like image 618
Gili Avatar asked Jul 29 '18 04:07

Gili


People also ask

What happens when Lambda throws exception?

Rules for Lambda ExpressionA lambda expression cannot throw any checked exception until its corresponding functional interface declares a throws clause. An exception thrown by any lambda expression can be of the same type or sub-type of the exception declared in the throws clause of its functional interface.

What happens with Lambda after compile time?

For Lambda expressions, the compiler doesn't translate them into something which is already understood by JVM. Lambda syntax that is written by the developer is desugared into JVM level instructions generated during compilation, which means the actual responsibility of constructing lambda is deferred to runtime.

How does Lambda work in JVM?

Lambda expression is compiled using invokedynamic bytecode. Lambda implementation is stored in the same class file as a special private method. Whether an object is created to invoke lambda depends on the situation. In the trivial cases lambda gets translated to a constant method handle.

How does Java Lambda work?

A lambda expression is a short block of code which takes in parameters and returns a value. Lambda expressions are similar to methods, but they do not need a name and they can be implemented right in the body of a method.


1 Answers

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.

like image 85
zhh Avatar answered Sep 23 '22 01:09

zhh