web-dev-qa-db-ja.com

Java 8ジェネリック型推論がこのオーバーロードを選択するのはなぜですか?

次のプログラムを検討してください。

_public class GenericTypeInference {

    public static void main(String[] args) {
        print(new SillyGenericWrapper().get());
    }

    private static void print(Object object) {
        System.out.println("Object");
    }

    private static void print(String string) {
        System.out.println("String");
    }

    public static class SillyGenericWrapper {
        public <T> T get() {
            return null;
        }
    }
}
_

Java 8の下に「文字列」、Java 7の下に「オブジェクト」を出力します。

両方のオーバーロードされたメソッドが一致するため、これはJava 8ではあいまいであると予想していました。 JEP 101 の後にコンパイラがprint(String)を選択するのはなぜですか?

正当化されているかどうかにかかわらず、これは下位互換性を損ない、コンパイル時に変更を検出できません。 Java 8にアップグレードすると、コードの動作がおかしくなります。

注:SillyGenericWrapperは、理由のために「愚かな」と名付けられています。コンパイラーが動作する理由を理解しようとしていますが、そもそも馬鹿げたラッパーが悪いデザインだとは言わないでください。

更新:Java 8でサンプルをコンパイルして実行しようとしましたが、Java 7言語レベルを使用しました。動作はJava 7と一致していました。これは予想されていましたが、まだ確認する必要があると感じました。

37
Bogdan Calmac

型推論のルールは、Java 8;で最も重要なターゲット型推論が大幅に改善されました。そのため、Java 8以前はメソッドJava 8では、引数サイトはデフォルトでObjectを推測しませんでした。この場合、最も具体的な適用可能な型が推測されます。この場合はStringです。JavaのJLS 8は新しい章を導入しました 第18章。型推論 Java 7。

JDK 1.8の以前のバージョン(1.8.0_25まで)には、JLSに従って曖昧性エラーを生成するはずのコードをコンパイラが正常にコンパイルしたときに、オーバーロードされたメソッドの解決に関連するバグがありました なぜこのメソッドはあいまいなオーバーロードなのですか? As Marco13がコメントで指摘

JLSのこの部分はおそらく最も複雑な部分です

これは、JDK 1.8の以前のバージョンのバグと、表示される互換性の問題について説明しています。


Java Tutoral( Type Inference )の例に示すように

次の方法を検討してください。

_void processStringList(List<String> stringList) {
    // process stringList
}
_

空のリストを指定してprocessStringListメソッドを呼び出すとします。 Java SE 7では、次のステートメントはコンパイルされません。

_processStringList(Collections.emptyList());
_

Java SE 7コンパイラは、次のようなエラーメッセージを生成します。

_List<Object> cannot be converted to List<String>
_

コンパイラーは、型引数Tの値を必要とするため、値Objectで始まります。したがって、Collections.emptyListを呼び出すと、リストのタイプの値が返されます。これは、メソッドprocessStringListと互換性がありません。したがって、Java SE 7では、type引数の値を次のように指定する必要があります。

_processStringList(Collections.<String>emptyList());
_

これは、Java SE 8では不要になりました。ターゲットタイプとは何かという概念が拡張され、メソッドprocessStringListへの引数などのメソッド引数が含まれるようになりました。この場合、processStringListはリスト型の引数

Collections.emptyList()は、質問のget()メソッドに類似したジェネリックメソッドです。 In Java 7では、print(String string)メソッドはメソッド呼び出しにも適用できないため、過負荷解決プロセスに参加しません。Java 8では、両方の方法が適用可能です。

この非互換性は JDK 8の互換性ガイド で言及する価値があります。


オーバーロードされたメソッドの解決に関連する同様の質問については、私の回答を確認できます Java 8 ternary conditional and unboxedプリミティブ)によるメソッドオーバーロードのあいまいさ

JLS 15.12.2.5最も具体的な方法の選択 によると:

複数のメンバーメソッドがアクセス可能であり、メソッド呼び出しに適用できる場合、ランタイムメソッドディスパッチの記述子を提供するために1つを選択する必要があります。 Javaプログラミング言語は、最も具体的な方法が選択されるという規則を使用します。

次に:

次のいずれかに該当する場合、引数式e1、...、ekを使用した呼び出しでは、1つの適用可能なメソッドm1が別の適用可能なメソッドm2よりも具体的です。

  1. m2は総称的であり、§18.5.4により、引数式e1、...、ekについてm1はm2よりも具体的であると推定されています。

  2. m2はジェネリックではなく、m1とm2は厳密または緩い呼び出しによって適用可能であり、m1には仮パラメータータイプS1、...、Snおよびm2には仮パラメータータイプT1、...、Tnがあり、タイプSiはより多くすべてのi(1≤i≤n、n = k)の引数eiのTiに固有。

  3. m2はジェネリックではなく、m1とm2は変数アリティの呼び出しで適用できます。ここで、m1の最初のk変数アリティパラメータタイプはS1、...、Skで、m2の最初のk変数アリティパラメータタイプはT1です。 。、Tk、すべてのi(1≤i≤k)の引数eiについて、Si型はTiよりも具体的です。さらに、m2にk + 1パラメータがある場合、m + 1のk + 1番目の可変アリティパラメータタイプは、m2のk + 1番目の可変アリティパラメータタイプのサブタイプです。

上記の条件は、ある方法が別の方法よりも具体的である唯一の状況です。

タイプSは、S <:T(§4.10)の場合、任意の式のタイプTよりも具体的です。

3つのオプションの2番目は、私たちのケースと一致します。 StringObject(_String <: Object_)のサブタイプなので、より具体的です。したがって、メソッド自体はより具体的です。 JLSに続いて、このメソッドもより厳密におよび最も固有であり、コンパイラによって選択されます。

19
medvedev1088

Java7では、式はボトムアップで解釈されます(例外はほとんどありません)。部分式の意味は、「文脈自由」のようなものです。メソッド呼び出しの場合、引数のタイプは最初に解決されます。次に、コンパイラーはその情報を使用して呼び出しの意味を解決します。たとえば、該当するオーバーロードされたメソッドの中から勝者を選択します。

Java8では、その哲学はもはや機能しません。なぜなら、どこでも暗黙的なラムダ(x->foo(x)など)を使用することを期待しているためです。ラムダパラメータタイプは指定されておらず、コンテキストから推測する必要があります。つまり、メソッド呼び出しの場合、メソッドのパラメーターの型が引数の型を決定することがあります。

メソッドがオーバーロードされている場合、明らかにジレンマがあります。したがって、場合によっては、引数をコンパイルする前に、メソッドのオーバーロードを解決して、勝者を1つ選択する必要があります。

これは大きな変化です。そして、あなたのようないくつかの古いコードは非互換性の犠牲になります。

回避策は、「キャストコンテキスト」を使用して引数に「ターゲットタイプ」を提供することです

_    print( (Object)new SillyGenericWrapper().get() );
_

または、@ Holgerの提案のように、型パラメーター<Object>get()を提供して、すべてが同時に推論されるのを回避します。


Javaメソッドのオーバーロードは非常に複雑です。複雑さの利点は疑わしいものです。オーバーロードは必ずしも必要ではありません-それらが異なるメソッドである場合、それらに異なる名前を付けることができます。

6
ZhongYu

まず、それはoverridingとは何の関係もありませんが、オーバーロードに対処する必要があります。

Jls 、. セクション15 は、コンパイラーがオーバーロードされたメソッドを正確に選択する方法に関する多くの情報を提供します

最も具体的な方法はコンパイル時に選択されます。その記述子は、実行時に実際に実行されるメソッドを決定します。

だから呼び出すとき

print(new SillyGenericWrapper().get());

Stringを使用するObjectメソッドはprintを使用するメソッドよりも具体的であるため、コンパイラはStringよりもObjectバージョンを選択します。 Integerの代わりにStringがあった場合、それが選択されます。

さらに、Objectをパラメーターとして取るメソッドを呼び出す場合は、戻り値をobjectタイプのパラメーターに割り当てることができます。

public class GenericTypeInference {

    public static void main(String[] args) {
        final SillyGenericWrapper sillyGenericWrapper = new SillyGenericWrapper();
        final Object o = sillyGenericWrapper.get();
        print(o);
        print(sillyGenericWrapper.get());
    }

    private static void print(Object object) {
        System.out.println("Object");
    }

    private static void print(Integer integer) {
        System.out.println("Integer");
    }

    public static class SillyGenericWrapper {
        public <T> T get() {
            return null;
        }
    }
}

出力します

Object
Integer

オーバーロードの対象となる2つの有効なメソッド定義があるとすると、状況は興味深いものになります。例えば。

private static void print(Integer integer) {
    System.out.println("Integer");
}

private static void print(String integer) {
    System.out.println("String");
}

そして今あなたが呼び出す場合

print(sillyGenericWrapper.get());

コンパイラーには、2つの有効なメソッド定義から選択できます。したがって、一方のメソッドをもう一方のメソッドよりも優先させることができないため、コンパイルエラーが発生します。

2
sol4me

Java 1.8.0_40を使用して実行し、「オブジェクト」を取得しました。

次のコードを実行するとします。

public class GenericTypeInference {

private static final String fmt = "%24s: %s%n";
public static void main(String[] args) {

    print(new SillyGenericWrapper().get());

    Method[] allMethods = SillyGenericWrapper.class.getDeclaredMethods();
    for (Method m : allMethods) {
        System.out.format("%s%n", m.toGenericString());
        System.out.format(fmt, "ReturnType", m.getReturnType());
        System.out.format(fmt, "GenericReturnType", m.getGenericReturnType());   
   }

   private static void print(Object object) {
       System.out.println("Object");
   }

   private static void print(String string) {
       System.out.println("String");
   }

   public static class SillyGenericWrapper {
       public <T> T get() {
           return null;
       }
   }
}

あなたはあなたが得ることがわかります:

オブジェクトpublic T com.xxx.GenericTypeInference $ SillyGenericWrapper.get()ReturnType:class Java.lang.Object GenericReturnType:T

これは、Stringのメソッドではなく、Objectでオーバーロードされたメソッドが使用される理由を説明しています。

1
Daniel L.