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

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.


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

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.