Boolean, operator kondisional, dan autoboxing

132

Mengapa ini melempar NullPointerException

public static void main(String[] args) throws Exception {
    Boolean b = true ? returnsNull() : false; // NPE on this line.
    System.out.println(b);
}

public static Boolean returnsNull() {
    return null;
}

sementara ini tidak

public static void main(String[] args) throws Exception {
    Boolean b = true ? null : false;
    System.out.println(b); // null
}

?

Solusinya adalah dengan cara mengganti falsedengan Boolean.FALSEuntuk menghindari nullunboxed ke boolean- yang tidak mungkin. Tapi itu bukan pertanyaannya. Pertanyaannya adalah mengapa ? Apakah ada referensi di JLS yang mengkonfirmasi perilaku ini, terutama dari kasus ke-2?

BalusC
sumber
28
wow, autoboxing adalah sumber ... er ... kejutan untuk programmer java, bukan? :-)
leonbloy
Saya memiliki masalah yang sama dan yang mengejutkan saya adalah gagal pada OpenJDK VM tetapi bekerja pada HotSpot VM ... Tulis sekali, jalankan di mana saja!
kodu

Jawaban:

92

Perbedaannya adalah bahwa jenis eksplisit returnsNull()metode ini mempengaruhi pengetikan statis ekspresi pada waktu kompilasi:

E1: `true ? returnsNull() : false` - boolean (auto-unboxing 2nd operand to boolean)

E2: `true ? null : false` - Boolean (autoboxing of 3rd operand to Boolean)

Lihat Spesifikasi Bahasa Jawa, bagian 15.25 Operator Bersyarat? :

  • Untuk E1, jenis operan ke-2 dan ke-3 adalah masing Boolean- booleanmasing, jadi klausa ini berlaku:

    Jika salah satu dari operan kedua dan ketiga adalah tipe boolean dan tipe lainnya adalah tipe Boolean, maka tipe ekspresi kondisional adalah boolean.

    Karena jenis ekspresinya adalah boolean, operan ke-2 harus dipaksa boolean. Kompiler memasukkan kode buka-otomatis ke operan ke-2 (nilai balik dari returnsNull()) untuk membuatnya diketik boolean. Ini tentu saja menyebabkan NPE dari yang nulldikembalikan pada saat run-time.

  • Untuk E2, jenis operan ke-2 dan ke-3 <special null type>(tidak Booleanseperti pada E1!) Dan booleanmasing - masing, jadi tidak ada klausa pengetikan spesifik yang berlaku ( baca 'em! ), Jadi klausa "sebaliknya" yang berlaku berlaku:

    Jika tidak, operan kedua dan ketiga adalah tipe S1 dan S2 masing-masing. Misalkan T1 adalah tipe yang dihasilkan dari penerapan konversi tinju ke S1, dan biarkan T2 adalah tipe yang dihasilkan dari penerapan konversi tinju ke S2. Jenis ekspresi kondisional adalah hasil dari penerapan konversi tangkap (§5.1.10) ke lub (T1, T2) (§15.12.2.7).

    • S1 == <special null type>(lihat §4.1 )
    • S2 == boolean
    • T1 == kotak (S1) == <special null type>(lihat item terakhir dalam daftar konversi tinju di §5.1.7 )
    • T2 == kotak (S2) == `Boolean
    • lub (T1, T2) == Boolean

    Jadi jenis ekspresi kondisional adalah Booleandan operan ke-3 harus dipaksa Boolean. Kompiler memasukkan kode tinju otomatis untuk operan ke-3 ( false). Operan ke-2 tidak memerlukan auto-unboxing seperti pada E1, jadi tidak ada NPE auto-unboxing ketika nulldikembalikan.


Pertanyaan ini membutuhkan analisis jenis yang serupa:

Operator kondisional Java?: Jenis hasil

Bert F
sumber
4
Masuk akal ... saya pikir. The §15.12.2.7 adalah rasa sakit.
BalusC
Itu mudah ... tetapi hanya di belakang. :-)
Bert F
@BertF Untuk apa fungsi lubdalam lub(T1,T2)singkatan?
Geek
1
@ Geek - lub () - paling tidak terikat - pada dasarnya superkelas terdekat yang mereka miliki; karena null (ketik "tipe null khusus") dapat secara implisit dikonversi (diperlebar) ke jenis apa pun, Anda dapat menganggap jenis null khusus sebagai "superclass" dari semua jenis (kelas) untuk keperluan lub ().
Bert F
25

Garis:

    Boolean b = true ? returnsNull() : false;

diubah secara internal menjadi:

    Boolean b = true ? returnsNull().booleanValue() : false; 

untuk melakukan unboxing; jadi:null.booleanValue() akan menghasilkan NPE

Ini adalah salah satu jebakan utama saat menggunakan autoboxing. Perilaku ini memang didokumentasikan dalam 5.1.8 JLS

Sunting: Saya percaya bahwa unboxing adalah karena operator ketiga adalah tipe boolean, seperti (pemeran implisit ditambahkan):

   Boolean b = (Boolean) true ? true : false; 
jjungnickel
sumber
2
Mengapa ia mencoba membuka kotak seperti itu, ketika nilai akhir adalah objek Boolean?
Erick Robertson
16

Dari Spesifikasi Bahasa Jawa, bagian 15.25 :

  • Jika salah satu dari operan kedua dan ketiga adalah tipe boolean dan tipe lainnya adalah tipe Boolean, maka tipe ekspresi kondisional adalah boolean.

Jadi, yang pertama contoh mencoba untuk memanggil Boolean.booleanValue()untuk mengkonversi Booleanke booleansesuai aturan pertama.

Dalam kasus kedua operan pertama adalah dari jenis nol, ketika yang kedua bukan dari jenis referensi, jadi konversi autoboxing diterapkan:

  • Jika tidak, operan kedua dan ketiga adalah tipe S1 dan S2. Misalkan T1 adalah tipe yang dihasilkan dari penerapan konversi tinju ke S1, dan biarkan T2 adalah tipe yang dihasilkan dari penerapan konversi tinju ke S2. Jenis ekspresi kondisional adalah hasil dari penerapan konversi tangkap (§5.1.10) ke lub (T1, T2) (§15.12.2.7).
axtavt
sumber
Ini menjawab kasus pertama, tetapi bukan kasus kedua.
BalusC
Mungkin ada pengecualian untuk saat salah satu nilainya null.
Erick Robertson
@Erick: apakah JLS mengkonfirmasi ini?
BalusC
1
@ Erick: Saya rasa ini tidak berlaku karena booleanbukan tipe referensi.
axtavt
1
Dan boleh saya tambahkan ... ini sebabnya Anda harus membuat kedua sisi dari jenis yang sama, dengan panggilan eksplisit jika perlu. Bahkan jika Anda memiliki spesifikasi yang dihafal dan tahu apa yang akan terjadi, programmer berikutnya untuk datang dan membaca kode Anda mungkin tidak. Menurut pendapat saya, akan lebih baik jika kompiler hanya menghasilkan pesan kesalahan dalam situasi ini daripada melakukan hal-hal yang sulit diprediksi oleh manusia biasa. Yah, mungkin ada kasus-kasus di mana perilakunya benar-benar bermanfaat, tetapi saya belum menemukannya.
Jay
0

Kita dapat melihat masalah ini dari kode byte. Pada baris 3 kode byte utama 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Z,, Boolean nilai nol, invokevirtualmetode java.lang.Boolean.booleanValue, itu akan melempar NPE tentu saja.

    public static void main(java.lang.String[]) throws java.lang.Exception;
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=2, locals=2, args_size=1
           0: invokestatic  #2                  // Method returnsNull:()Ljava/lang/Boolean;
           3: invokevirtual #3                  // Method java/lang/Boolean.booleanValue:()Z
           6: invokestatic  #4                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;
           9: astore_1
          10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
          13: aload_1
          14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
          17: return
        LineNumberTable:
          line 3: 0
          line 4: 10
          line 5: 17
      Exceptions:
        throws java.lang.Exception

    public static java.lang.Boolean returnsNull();
      descriptor: ()Ljava/lang/Boolean;
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=1, locals=0, args_size=0
           0: aconst_null
           1: areturn
        LineNumberTable:
          line 8: 0
Yanhui Zhou
sumber