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

Gili :

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

duvduv :

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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=35084&siteId=1