複数の一致するターゲットタイプを持つラムダ式のメソッドシグネチャの選択


11

質問に答えていて説明できないシナリオに出くわしました。このコードを考えてみましょう:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

ラムダのパラメーターを明示的に入力(A a) -> aList.add(a)するとコードがコンパイルされる理由がわかりません。さらに、なぜそれがでIterableはなく過負荷にリンクするのCustomIterableですか?
これに対する説明や、仕様の関連セクションへのリンクはありますか?

注:拡張iterable.forEach((A a) -> aList.add(a));時にのみコンパイルしCustomIterable<T>ますIterable<T>(メソッドのフラットなオーバーロードによりCustomIterable、あいまいなエラーが発生します)。


両方でこれを取得する:

  • openjdkバージョン "13.0.2" 2020-01-14
    Eclipseコンパイラ
  • openjdkバージョン "1.8.0_232"
    Eclipseコンパイラ

編集:上記のコードは、Eclipseがコードの最後の行を正常にコンパイルしている間、mavenでビルドするとコンパイルに失敗します。


3
3つともJava 8でコンパイルされません。これが新しいバージョンで修正されたバグなのか、導入されたバグ/機能なのかわかりません... Javaバージョンを指定する必要があります
Sweeper

@Sweeper私は最初、jdk-13を使用してこれを取得しました。Java 8(jdk8u232)での後続のテストでは、同じエラーが表示されます。最後のものがあなたのマシンでコンパイルされない理由がわかりません。
ernest_k

2つのオンラインのコンパイラ(のいずれかに再現することはできません12)。私のマシンでは1.8.0_221を使用しています。これはますます奇妙になっています...
スイーパー

1
@ernest_k Eclipseには独自のコンパイラー実装があります。それは質問にとって重要な情報になる可能性があります。また、クリーンなmavenビルドエラーの最後の行も、私の意見の質問で強調表示する必要があります。一方、リンクされた質問については、コードが再現可能ではないため、OPがEclipseも使用しているという仮定を明確にすることができます。
ナマン

3
私はその質問の知的価値を理解していますが、関数型インターフェイスのみが異なるオーバーロードされたメソッドを作成せず、ラムダを渡して安全に呼び出すことができると期待することはできません。ラムダ型の推論とオーバーロードの組み合わせが、平均的なプログラマーが理解することになるとは思いません。これは、ユーザーが制御できない非常に多くの変数を含む方程式です。これを避けてください:)
ステファンヘルマン

回答:


8

TL; DR、これはコンパイラのバグです。

継承された場合に特定の適用可能なメソッドまたはデフォルトのメソッドを優先するルールはありません。興味深いことに、コードを次のように変更すると

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

iterable.forEach((A a) -> aList.add(a));声明は、Eclipseでエラーが発生します。

別のオーバーロードを宣言するときにインターフェースのforEach(Consumer<? super T) c)メソッドのプロパティはIterable<T>変更されなかったため、このメソッドを選択するというEclipseの決定は、(一貫して)メソッドのプロパティに基づくことはできません。継承された唯一のdefaultメソッドであり、唯一のメソッドであり、唯一のJDKメソッドなどです。いずれにしても、これらのプロパティのいずれもメソッドの選択に影響を与えるべきではありません。

宣言を

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

また、「あいまいな」エラーが生成されるため、適用可能なオーバーロードされたメソッドの数も関係ありません。候補が2つしかない場合でも、defaultメソッドに対する一般的な優先順位はありません。

これまでのところ、2つの適用可能なメソッドがあり、defaultメソッドと継承関係が関係しているときに問題が発生するようですが、これはさらに掘り下げるには適切な場所ではありません。


しかし、例の構成要素がコンパイラの異なる実装コードによって処理される可能性があることは理解できます。1つはバグを示し、もう1つはバグを示しません。
a -> aList.add(a)ある暗黙に型付けされたオーバーロードの解決のために使用することはできませんラムダ式は、。これとは対照的に、(A a) -> aList.add(a)ある明示的に型指定されたオーバーロードされたメソッドからのマッチング方法を選択するために使用することができますラムダ式が、それはここで助けにはならない(はずここではないヘルプ)、すべてのメソッドはまったく同じ機能シグネチャとパラメータ型を持っているように。

反例として、

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

関数シグネチャは異なり、明示的に型指定されたラムダ式を使用すると正しい方法を選択するのに役立ちますが、暗黙的に型指定されたラムダ式は役に立たないため、forEach(s -> s.isEmpty())コンパイラエラーが発生します。そして、すべてのJavaコンパイラはこれについて同意します。

メソッドは多重定義さaList::addれているため、あいまいなメソッド参照であることに注意してください。メソッドのadd選択にも役立ちませんが、メソッド参照はとにかく別のコードで処理される可能性があります。明確に切り替えるか、明確にaList::containsするListためCollectionににadd変更しても、Eclipseインストールの結果は変わりませんでした(私はを使用しました2019-06)。


1
@howlgerコメントは意味がありません。デフォルトのメソッド継承されたメソッドであり、メソッドはオーバーロードされています。他に方法はありません。継承されたメソッドがメソッドであるという事実は、default単なる追加ポイントです。私の答えは、Eclipseがデフォルトのメソッドを優先しない例をすでに示しています。
Holger

1
@howlger私たちの行動には根本的な違いがあります。削除defaultすると結果が変わることを発見し、観察された動作の理由をすぐに見つけたと想定しました。あなたはそれについて非常に自信を持っているので、他の答えは間違っていないとしても、彼らは矛盾していません。あなたが他の人よりもあなた自身の行動を投影しているので、私は継承が理由であると主張しませんでした。そうではないことを証明しました。Eclipseは、3つのオーバーロードが存在する1つのシナリオでは特定のメソッドを選択し、別のシナリオでは選択しないため、動作に一貫性がないことを示しました。
ホルガー

1
@howlgerに加えて、私はこのコメントの最後に別のシナリオに名前を付けました。1つのインターフェース、継承なし、2つのメソッド、もう1つdefaultabstract2つのコンシューマーのような引数を作成して試してみてください。Eclipseは、1つはdefaultメソッドですが、あいまいであると正しく述べています。どうやら継承はまだこのEclipseバグに関連しているようですが、あなたとは異なり、私は怒って他の答えを間違っているとは思っていません。それはここでは私たちの仕事ではありません。
Holger

1
@howlgerいいえ、ポイントはこれがバグであることです。ほとんどの読者は細部についてさえ気にしません。Eclipseは、メソッドを選択するたびにサイコロを振ることができますが、それは問題ではありません。あいまいな場合、Eclipseはメソッドを選択すべきではないため、なぜメソッドを選択するかは問題ではありません。この回答は、動作に一貫性がないことを証明しています。これは、バグであることを強く示すのにすでに十分です。Eclipseのソースコードのどこに問題があるかを指摘する必要はありません。それはStackoverflowの目的ではありません。おそらく、あなたはStackoverflowをEclipseのバグトラッカーと混同しているでしょう。
Holger

1
あなたはしている@howlger 再び誤っ私はEclipseはその間違った選択をした理由の(間違った)声明を発表したと主張します。繰り返しますが、日食はまったく選択をするべきではないので、私はしませんでした。メソッドがあいまいです。ポイント。「継承」という用語を使用したのは、同じ名前のメソッドを区別する必要があったためです。代わりに、ロジックを変更せずに、「デフォルトの方法」と言ったかもしれません。もっと正確に言えば、「何らかの理由でEclipseが誤って選択したまさにその方法」というフレーズを使用するべきでした。3つのフレーズのどちらも同じ意味で使用でき、ロジックは変わりません。
Holger

2

Java言語仕様15.12.2.5によると、これは最も具体的なメソッドであるためdefault Eclipseコンパイラーは正しくメソッドを解決します

abstract最も具体的な方法の1つが具体的(つまり、非デフォルト)である場合、それが最も具体的な方法です。

javac(デフォルトでMavenとIntelliJによって使用されます)メソッド呼び出しがここではあいまいであることを示します。しかし、Java言語仕様によると、2つのメソッドの1つがここで最も具体的なメソッドであるため、あいまいではありません。

暗黙的に型指定されたラムダ式は、Javaで明示的に型指定されたラムダ式とは異なる方法で処理されます。明示的に型指定されたラムダ式とは対照的に、暗黙的に型指定されたものは、厳密な呼び出しのメソッドを識別するために、最初のフェーズを通過します(Java言語仕様jls-15.12.2.2の最初のポイントを参照)。したがって、ここでのメソッド呼び出しは、暗黙的に型指定されたラムダ式ではあいまいです

あなたの場合、このバグの回避策は、次のように明示的に型指定されたラムダ式を使用する代わりに、機能インターフェイスの型javacを指定することです。

iterable.forEach((ConsumerOne<A>) aList::add);

または

iterable.forEach((Consumer<A>) aList::add);

テスト用にさらに最小化した例を次に示します。

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}

4
引用された文の直前の前提条件を逃しました:「最大限に特定されたすべてのメソッドにオーバーライドと同等のシグネチャがある場合」もちろん、引数がまったく無関係の2つのメソッドには、オーバーライドと同等のシグネチャがありません。さらに、default候補となるメソッドが3つある場合、または両方のメソッドが同じインターフェースで宣言されている場合に、Eclipseがメソッドの選択を停止する理由を説明していません。
ホルガー

1
@ホルガーあなたの答えは、「それが継承されたときに特定の適用可能なメソッドまたはデフォルトのメソッドに優先権を与えるルールはない」と主張しています。この存在しない規則の前提条件はここでは適用されないと言っていることを正しく理解していますか?ここでのパラメーターは機能的なインターフェースです(JLS 9.8を参照)。
ハウガー

1
あなたは文を文脈から引き裂いた。この文は、オーバーライドに相当するメソッドの選択、つまり、具象クラスには具象メソッドが1つしかないため、実行時にすべて同じメソッドを呼び出す宣言間の選択について説明しています。これはforEach(Consumer)、やのような別個のメソッドの場合には関係forEach(Consumer2)ありません。
Holger

2
@StephanHerrmann私はJEPやJSRを知らないが、修正などの変更ルックスは「具体的な」の意味に沿ったものであることを、すなわちと比較JLS§9.4:「デフォルトの方法は、具体的な方法(§8.4とは区別されます。 3.1)、クラスで宣言されています。」、変更されませんでした。
ホルガー

2
@StephanHerrmannはい、オーバーライドに相当するメソッドから候補を選択することがより複雑になり、その背後にある理論的根拠を知るのは興味深いでしょうが、これは当面の質問には関係ありません。変更と動機を説明する別のドキュメントがあるはずです。以前はありましたが、この「 "年ごとの新しいバージョン」ポリシーでは、品質を維持することは不可能に思われます...
ホルガー

2

EclipseがJLS§15.12.2.5を実装するコードでは、明示的に型指定されたラムダの場合でも、どちらの方法も他よりも具体的であるとは見なされません。

したがって、理想的には、Eclipseはここで停止し、あいまいさを報告します。残念ながら、オーバーロード解決の実装には、JLSの実装に加えて重要なコードがあります。私の理解では、このコード(Java 5が新しくなったときからのもの)は、JLSのいくつかのギャップを埋めるために保持する必要があります。

これを追跡するために、https://bugs.eclipse.org/562538を提出しました。

この特定のバグとは関係なく、私はこのスタイルのコードに対してのみ強くアドバイスすることができます。オーバーロードは、ラムダ型の推論を乗じた、Javaでの多くの驚きに適しています。複雑さは、知覚される利得とは比例していません。


ありがとうございました。bugs.eclipse.org/bugs/show_bug.cgi?id=562507がすでにログに記録されていた場合、それらをリンクするか、重複して閉じることができます...
ernest_k
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.