web-dev-qa-db-ja.com

ラムダがランタイム例外をスローするときにオーバーロードを変更するのはなぜですか?

私と一緒に、導入は少し長めですが、これは面白いパズルです。

私はこのコードを持っています:

_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」の両方のカテゴリに分類されるようです。したがって、明らかにこの場合にはあいまいさがありますが、なぜSupplierRunnableよりも過負荷に適しているのか、私は確かに理解していません。前者が例外をスローするのとは異なり、後者は例外をスローしません。

この場合に何が起こるべきかを言うほど、仕様について十分に理解していません。

https://bugs.openjdk.Java.net/browse/JDK-820849 に表示されるバグレポートを提出しました

47
Gili

まず、 §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よりも具体的です。

させてください:

  • [〜#〜] s [〜#〜]:= _Supplier<T>_
  • [〜#〜] t [〜#〜]:= Runnable
  • e:= _() -> { throw ... }_

そのため:

  • MTs:= T get() ==>Rs := T
  • MTt:= void run() ==>Rt := void

そして:

  • Sは、Tのスーパーインターフェースまたはサブインターフェースではありません
  • MTsMTtには同じタイプのパラメーターがあります(なし)
  • 正式なパラメーターがないため、箇条書き3も当てはまります
  • eは明示的に型指定されたラムダ式であり、Rtvoid
10
duvduv

問題は、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を選択します。

19
zhh

例外をスローすると、コンパイラは参照を返すインターフェイスを選択するようです。

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);
}

うまくコンパイルします。

とても奇妙です。

  • voidintはあいまいです
  • intIntegerはあいまいです
  • voidIntegerはあいまいではありません。

ここで何かが壊れていると思います。

Oracleにバグレポートを送信しました。

7
Peter Lawrey

まず最初に:

重要な点は、同じ引数位置に異なる機能インターフェイスを持つメソッドまたはコンストラクターをオーバーロードすると混乱が生じることです。 したがって、メソッドをオーバーロードして、同じ引数の位置で異なる機能的なインターフェイスを使用しないでください

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-compatiblevalue-compatible、潜在的な適用可能性の定義は、基本的なアリティチェックを超えて、機能アカウントのターゲットタイプの存在とshapeも考慮に入れます。

5

私はこれをバグと誤って考えていましたが、 §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を参照し、元のレポートに誤りがあると信じていることを示す更新を送信しました。

2
David Conrad