私と一緒に、導入は少し長めですが、これは面白いパズルです。
私はこのコードを持っています:
_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(); }
}
}
}
_
タスクをキューに追加し、順番に実行しようとしています。 3つのケースすべてがadd(Runnable)
メソッドを呼び出すことを期待していました。ただし、実際に発生するのは、ケース2がCompletionStage
を返す前に例外をスローする_Supplier<CompletionStage<Void>>
_として解釈されるため、「これは発生しない」コードブロックがトリガーされ、ケース3は実行されないことです。
デバッガーを使用してコードをステップ実行することで、ケース2が間違ったメソッドを呼び出していることを確認しました。
2番目のケースでRunnable
メソッドが呼び出されないのはなぜですか?
どうやらこの問題はJava 10以降でのみ発生するため、この環境でテストしてください。
[〜#〜] update [〜#〜]: JLS§15.12.2.1。潜在的に適用可能なメソッドの特定 およびより具体的には JLS§15.27.2。Lambda Body _() -> { throw new RuntimeException(); }
_は「void-compatible」と「value-compatible」の両方のカテゴリに分類されるようです。したがって、明らかにこの場合にはあいまいさがありますが、なぜSupplier
がRunnable
よりも過負荷に適しているのか、私は確かに理解していません。前者が例外をスローするのとは異なり、後者は例外をスローしません。
この場合に何が起こるべきかを言うほど、仕様について十分に理解していません。
https://bugs.openjdk.Java.net/browse/JDK-820849 に表示されるバグレポートを提出しました
まず、 §15.27.2 によると:
_() -> { throw ... }
_
void
互換性と値互換性の両方があるため、_Supplier<CompletionStage<Void>>
_と互換性があります( §15.27. )。
_class Test {
void foo(Supplier<CompletionStage<Void>> bar) {
throw new RuntimeException();
}
void qux() {
foo(() -> { throw new IllegalArgumentException(); });
}
}
_
(コンパイルされることを参照)
次に、 §15.12.2.5 _Supplier<T>
_(T
は参照型)によると、Runnable
よりも具体的です。
させてください:
Supplier<T>
_Runnable
() -> { throw ... }
_そのため:
T get()
==>Rs := T
void run()
==>Rt := void
そして:
S
は、T
のスーパーインターフェースまたはサブインターフェースではありませんvoid
問題は、2つの方法があることです。
void fun(Runnable r)
およびvoid fun(Supplier<Void> s)
。
そして、式fun(() -> { throw new RuntimeException(); })
。
どのメソッドが呼び出されますか?
JLS§15.12.2.1 によると、ラムダ本体はvoid互換性と値互換性の両方があります。
Tの関数型にvoidリターンがある場合、ラムダ本体はステートメント式(§14.8)またはvoid互換ブロック(§15.27.2)のいずれかです。
Tの関数型に(非void)戻り型がある場合、ラムダ本体は式または値互換ブロック(§15.27.2)のいずれかです。
したがって、両方のメソッドはラムダ式に適用できます。
ただし、2つのメソッドがあるため、Javaコンパイラーはどちらのメソッドがより具体的かを調べる必要があります
JLS§15.12.2.5 で。それは言います:
次のすべてが当てはまる場合、機能インターフェイスタイプSは、式eの機能インターフェイスタイプTよりも具体的です。
次のいずれかです。
RSをMTSの戻り値の型とし、MTTの型パラメーターに適合させ、RTをMTTの戻り値の型とします。次のいずれかが真でなければなりません。
次のいずれかです。
RTは無効です。
Supplier
のメソッドの戻り値の型はRunnable
であるため、S(つまりRunnable
)はT(つまりvoid
)よりも具体的です。
そのため、コンパイラはSupplier
ではなくRunnable
を選択します。
例外をスローすると、コンパイラは参照を返すインターフェイスを選択するようです。
interface Calls {
void add(Runnable run);
void add(IntSupplier supplier);
}
// Ambiguous call
calls.add(() -> {
System.out.println("hi");
throw new IllegalArgumentException();
});
しかしながら
interface Calls {
void add(Runnable run);
void add(IntSupplier supplier);
void add(Supplier<Integer> supplier);
}
文句を言う
エラー:(24、14)Java:追加への参照があいまいですMain.Callsのメソッドadd(Java.util.function.IntSupplier)とMain.Callsのメソッドadd(Java.util.function.Supplier)が一致しています
最後に
interface Calls {
void add(Runnable run);
void add(Supplier<Integer> supplier);
}
うまくコンパイルします。
とても奇妙です。
void
対int
はあいまいですint
対Integer
はあいまいですvoid
対Integer
はあいまいではありません。ここで何かが壊れていると思います。
Oracleにバグレポートを送信しました。
まず最初に:
重要な点は、同じ引数位置に異なる機能インターフェイスを持つメソッドまたはコンストラクターをオーバーロードすると混乱が生じることです。 したがって、メソッドをオーバーロードして、同じ引数の位置で異なる機能的なインターフェイスを使用しないでください
Joshua Bloch、-効果的なJava
それ以外の場合は、正しいオーバーロードを示すキャストが必要です。
_queue.add((Runnable) () -> { throw new IllegalArgumentException(); });
^
_
ランタイム例外の代わりに無限ループを使用する場合、同じ動作が明らかです。
_queue.add(() -> { for (;;); });
_
上記の場合、ラムダ本体は正常に完了しないため、混乱が増します:(void-compatibleまたはvalue-compatibleを選択するオーバーロード)ラムダが暗黙的に型付けされている場合?この状況では、両方の方法が適用可能になるため、たとえば次のように記述できます。
_queue.add((Runnable) () -> { throw new IllegalArgumentException(); });
queue.add((Supplier<CompletionStage<Void>>) () -> {
throw new IllegalArgumentException();
});
void add(Runnable task) { ... }
void add(Supplier<CompletionStage<Void>> task) { ... }
_
そして、これで述べられているように answer -曖昧な場合に最も具体的な方法が選択されます:
_queue.add(() -> { throw new IllegalArgumentException(); });
↓
void add(Supplier<CompletionStage<Void>> task);
_
同時に、ラムダ本体が正常に完了したとき(およびvoid互換のみ):
_queue.add(() -> { for (int i = 0; i < 2; i++); });
queue.add(() -> System.out.println());
_
この場合はあいまいさがないため、メソッドvoid add(Runnable task)
が選択されます。
JLS§15.12.2.1 で述べたように、ラムダ本体がvoid-compatibleとvalue-compatible、潜在的な適用可能性の定義は、基本的なアリティチェックを超えて、機能アカウントのターゲットタイプの存在とshapeも考慮に入れます。
私はこれをバグと誤って考えていましたが、 §15.27.2 に従って正しいようです。考慮してください:
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
これは、jdk-11-ea + 24、jdk-10.0.1、およびjdk1.8u181で発生します。
zhhの答えにより、このさらに簡単なテストケースを見つけることができました。
import Java.util.function.Supplier;
public class Simpler {
public static void main(String[] args) {
Supplier<Integer> s = () -> { throw new RuntimeException(); };
}
}
しかし、duvduvは§15.27.2、特にこの規則を指摘しました:
ブロックラムダ本体は、正常に完了できず(§14.21)、ブロック内のすべてのreturnステートメントの形式がreturnExpression;である場合、値互換性があります。
したがって、ブロックlambdaは、returnステートメントがまったく含まれていない場合でも、値の互換性は自明です。コンパイラはその型を推測する必要があるため、少なくとも1回のリターンExpression;が必要だと思っていたでしょう。ホルガーと他の人は、これが次のような通常の方法では必要ないことを指摘しています。
int foo() { for(;;); }
ただし、その場合、コンパイラは明示的な戻り値の型と矛盾する戻り値がないことを確認するだけで済みます。型を推測する必要はありません。ただし、JLSのルールは、ブロックラムダでも通常のメソッドと同じ自由度を許可するように記述されています。おそらく、私はもっと早くそれを見るべきだったでしょうが、私はしませんでした。
Oracleのバグ を提出しましたが、その後、§15.27.2を参照し、元のレポートに誤りがあると信じていることを示す更新を送信しました。