问题
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
回答1:
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 ofT
- 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
回答2:
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
.
回答3:
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
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.
回答4:
First things first:
The key point is that overloading methods or constructors with different functional interfaces in the same argument position causes confusion. Therefore, do not overload methods to take different functional interfaces in the same argument position.
Joshua Bloch, - Effective Java.
Otherwise, you'll need a cast to indicate the correct overloading:
queue.add((Runnable) () -> { throw new IllegalArgumentException(); });
^
The same behavior is evident when using an infinite loop instead of a runtime exception:
queue.add(() -> { for (;;); });
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:
queue.add((Runnable) () -> { throw new IllegalArgumentException(); });
queue.add((Supplier<CompletionStage<Void>>) () -> {
throw new IllegalArgumentException();
});
void add(Runnable task) { ... }
void add(Supplier<CompletionStage<Void>> task) { ... }
And, like stated in this answer - the most specific method is chosen in case of ambiguity:
queue.add(() -> { throw new IllegalArgumentException(); });
↓
void add(Supplier<CompletionStage<Void>> task);
At the same time, when the lambda body completes normally (and is void-compatible only):
queue.add(() -> { for (int i = 0; i < 2; i++); });
queue.add(() -> System.out.println());
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.
回答5:
I wrongly considered this a bug, but it appears to be correct according to §15.27.2. Consider:
import java.util.function.Supplier;
public class Bug {
public static void method(Runnable runnable) { }
public static void method(Supplier<Integer> supplier) { }
public static void main(String[] args) {
method(() -> System.out.println());
method(() -> { throw new RuntimeException(); });
}
}
javac Bug.java javap -c Bug
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: invokestatic #3 // Method add:(Ljava/lang/Runnable;)V
8: invokedynamic #4, 0 // InvokeDynamic #1:get:()Ljava/util/function/Supplier;
13: invokestatic #5 // Method add:(Ljava/util/function/Supplier;)V
16: return
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:
import java.util.function.Supplier;
public class Simpler {
public static void main(String[] args) {
Supplier<Integer> s = () -> { throw new RuntimeException(); };
}
}
However, duvduv pointed out §15.27.2, in particular, this rule:
A block lambda body is value-compatible if it cannot complete normally (§14.21) and every return statement in the block has the form return Expression;.
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:
int foo() { for(;;); }
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.
来源:https://stackoverflow.com/questions/51577332/why-does-a-lambda-change-overloads-when-it-throws-a-runtime-exception