web-dev-qa-db-ja.com

JDK8とJDK10の三項演算子の動作の違い

次のコードを検討してください

public class JDK10Test {
    public static void main(String[] args) {
        Double d = false ? 1.0 : new HashMap<String, Double>().get("1");
        System.out.println(d);
    }
}

JDK8で実行すると、このコードはnullを出力しますが、JDK10ではこのコードはNullPointerExceptionになります

Exception in thread "main" Java.lang.NullPointerException
    at JDK10Test.main(JDK10Test.Java:5)

コンパイラーによって生成されるバイトコードは、JDK10コンパイラーによって生成される2つの追加命令を除き、ほぼ同じです。これらの命令は、オートボクシングに関連しており、NPEを担当しているようです。

15: invokevirtual #7                  // Method Java/lang/Double.doubleValue:()D
18: invokestatic  #8                  // Method Java/lang/Double.valueOf:(D)Ljava/lang/Double;

この動作はJDK10のバグですか、それとも動作をより厳密にするための意図的な変更ですか?

JDK8:  Java version "1.8.0_172"
JDK10: Java version "10.0.1" 2018-04-17
57
SerCe

これはバグであり、修正されたと思われます。 JLSによると、NullPointerExceptionを投げることは正しい動作のようです。

ここで起こっているのは、バージョン8で何らかの理由でコンパイラが実際の型引数ではなくメソッドの戻り値型で言及された型変数の境界を考慮したことだと思います。つまり、...get("1")Objectを返すと考えられます。これは、メソッドの消去、またはその他の理由を考慮している可能性があります。

以下の §15.26 からの抜粋で指定されているように、動作はgetメソッドの戻り値の型に依存する必要があります。

  • 2番目と3番目のオペランド式が両方ともnumeric式の場合、条件式は数値条件式です。

    条件を分類するために、次の式は数値式です。

    • […]

    • 選択された最も具体的なメソッド(§15.12.2.5)が数値型に変換可能な戻り型を持つメソッド呼び出し式(§15.12)。

      ジェネリックメソッドの場合、これはメソッドの型引数をインスタンス化する前の型であることに注意してください。

    • […]

  • それ以外の場合、条件式は参照条件式です。

[…]

数値条件式のタイプは、次のように決定されます。

  • […]

  • 2番目と3番目のオペランドの一方がプリミティブ型Tであり、もう一方の型がボクシング変換(§5.1.7)をTに適用した結果である場合、条件式はTです。

つまり、両方の式が数値型に変換可能で、一方がプリミティブ型でもう一方がボックス化されている場合、三項条件式の結果型はプリミティブ型になります。

(表15.25-Cはまた、三項式_boolean ? double : Double_の型が実際にdoubleであることを示しています。これも、ボックス化解除とスローが正しいことを意味します。)

getメソッドの戻り値の型が数値型に変換可能でない場合、三項条件式は「参照条件式」と見なされ、ボックス化解除は行われません。

また、メモ"汎用メソッドの場合、これはメソッドの型引数をインスタンス化する前の型です"はこのケースには適用すべきではありません。 _Map.get_は型変数を宣言しません したがって、JLSの定義による汎用メソッドではありません 。ただし、このメモwasは、Java 9(唯一の変更であるため、 JLS8を参照 )に追加されました。今日私たちが見ている行動をしてください。

_HashMap<String, Double>_の場合、getshouldの戻り型はDoubleになります。

以下は、コンパイラが実際の型引数ではなく型変数の境界を考慮しているという私の理論をサポートするMCVEです。

_class Example<N extends Number, D extends Double> {
    N nullAsNumber() { return null; }
    D nullAsDouble() { return null; }

    public static void main(String[] args) {
        Example<Double, Double> e = new Example<>();

        try {
            Double a = false ? 0.0 : e.nullAsNumber();
            System.out.printf("a == %f%n", a);
            Double b = false ? 0.0 : e.nullAsDouble();
            System.out.printf("b == %f%n", b);

        } catch (NullPointerException x) {
            System.out.println(x);
        }
    }
}
_

Java 8 でのそのプログラムの出力は:

_a == null
Java.lang.NullPointerException
_

つまり、e.nullAsNumber()e.nullAsDouble()は実際の戻り値の型が同じですが、e.nullAsDouble()のみが「数値式」と見なされます。メソッド間の唯一の違いは、バインドされた型変数です。

おそらく、さらに多くの調査を行うことができますが、調査結果を投稿したかったのです。私はかなり多くのことを試してみましたが、式が戻り型の型変数を持つメソッドである場合にのみ、バグ(つまり、ボックス化解除/ NPEなし)が発生するようだとわかりました。


興味深いことに、私は 次のプログラムもスローする in Java 8:

_import Java.util.*;

class Example {
    static void accept(Double d) {}

    public static void main(String[] args) {
        accept(false ? 1.0 : new HashMap<String, Double>().get("1"));
    }
}
_

これは、3項式がローカル変数に割り当てられているか、メソッドパラメーターに割り当てられているかによって、コンパイラの動作が実際に異なることを示しています。

(元々、コンパイラが三項式に与えている実際の型を証明するためにオーバーロードを使用したかったのですが、上記の違いを考えると、それは可能に見えません。しかし。)

47
Radiodef

JLS 10は条件演算子の変更を指定していないようですが、理論はあります。

JLS 8およびJLS 10によれば、2番目の式(_1.0_)がdouble型であり、3番目(new HashMap<String, Double>().get("1"))がDouble型である場合、条件式の結果はdouble型です。 Java 8のJVMは、Doubleを返すため、最初に_HashMap#get_の結果をunboxする理由はないことを知るのに十分賢いようです。 doubleを選択してから、Doubleに戻します(Doubleを指定したため)。

これを証明するには、例でDoubledoubleに変更し、NullPointerExceptionがスローされます(JDK 8で)。これは、ボックス化解除が発生し、null.doubleValue()が明らかにNullPointerExceptionをスローするためです。

_double d = false ? 1.0 : new HashMap<String, Double>().get("1");
System.out.println(d); // Throws a NullPointerException
_

これは10年で変更されたようですが、その理由を説明することはできません。

12
Jacob G.