次のコードを検討してください
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
これはバグであり、修正されたと思われます。 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>
_の場合、get
shouldの戻り型は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項式がローカル変数に割り当てられているか、メソッドパラメーターに割り当てられているかによって、コンパイラの動作が実際に異なることを示しています。
(元々、コンパイラが三項式に与えている実際の型を証明するためにオーバーロードを使用したかったのですが、上記の違いを考えると、それは可能に見えません。しかし。)
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
を指定したため)。
これを証明するには、例でDouble
をdouble
に変更し、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年で変更されたようですが、その理由を説明することはできません。