Mengapa secara eksplisit melempar NullPointerException daripada membiarkannya terjadi secara alami?

182

Saat membaca kode sumber JDK, saya merasa umum bahwa penulis akan memeriksa parameter jika mereka nol dan kemudian melemparkan NullPointerException () baru secara manual. Mengapa mereka melakukannya? Saya pikir tidak perlu melakukannya karena itu akan membuang NullPointerException () baru ketika memanggil metode apa pun. (Berikut adalah beberapa kode sumber HashMap, misalnya :)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}
LiJiaming
sumber
32
titik kunci dari coding adalah niat
Scary Wombat
19
Ini adalah pertanyaan yang sangat bagus untuk pertanyaan pertama Anda! Saya telah membuat beberapa perubahan kecil; Saya harap kamu tidak keberatan. Saya juga menghapus ucapan terima kasih dan catatan tentang itu menjadi pertanyaan pertama Anda karena biasanya hal semacam itu bukan bagian dari pertanyaan SO.
David Conrad
11
Saya C # konvensi adalah untuk meningkatkan ArgumentNullExceptiondalam kasus-kasus seperti itu (bukan NullReferenceException) - itu sebenarnya pertanyaan yang sangat bagus tentang mengapa Anda mengajukan NullPointerExceptionsecara eksplisit di sini (daripada yang berbeda).
EJoshuaS
21
@ EJoshuaS Ini adalah debat lama apakah akan melempar IllegalArgumentExceptionatau NullPointerExceptionuntuk argumen nol. Konvensi JDK adalah yang terakhir.
shmosel
33
Masalah NYATA adalah bahwa mereka membuang kesalahan dan membuang semua informasi yang menyebabkan kesalahan ini . Tampaknya ini adalah kode sumber yang sebenarnya . Bahkan pesan string berdarah tidak sederhana. Sedih.
Martin Ba

Jawaban:

254

Ada sejumlah alasan yang muncul di benak, beberapa terkait erat:

Gagal-cepat: Jika gagal, lebih baik cepat gagal daripada nanti. Ini memungkinkan masalah untuk ditangkap lebih dekat ke sumbernya, membuatnya lebih mudah untuk diidentifikasi dan pulih. Ini juga menghindari pemborosan siklus CPU pada kode yang pasti gagal.

Maksud: Melempar pengecualian secara eksplisit membuat jelas bagi pengelola bahwa kesalahan itu ada dengan sengaja dan penulis menyadari konsekuensinya.

Konsistensi: Jika kesalahan dibiarkan terjadi secara alami, itu mungkin tidak terjadi di setiap skenario. Jika tidak ada pemetaan yang ditemukan, misalnya, remappingFunctiontidak akan pernah digunakan dan pengecualian tidak akan dibuang. Memvalidasi input terlebih dahulu memungkinkan perilaku yang lebih deterministik dan dokumentasi yang lebih jelas .

Stabilitas: Kode berkembang seiring waktu. Kode yang mengalami pengecualian secara alami mungkin, setelah sedikit refactoring, berhenti melakukannya, atau melakukannya dalam keadaan yang berbeda. Melemparkannya secara eksplisit membuat perilaku cenderung berubah secara tidak sengaja.

shmosel
sumber
14
Juga: dengan cara ini lokasi dari mana pengecualian dilemparkan terkait dengan satu variabel yang diperiksa. Tanpa itu, pengecualian mungkin karena salah satu dari beberapa variabel menjadi nol.
Jens Schauder
45
Yang lain: jika Anda menunggu NPE terjadi secara alami, beberapa kode di antara mungkin sudah mengubah keadaan program Anda melalui efek samping.
Thomas
6
Meskipun cuplikan ini tidak melakukannya, Anda bisa menggunakan new NullPointerException(message)konstruktor untuk menjelaskan apa yang null. Bagus untuk orang yang tidak memiliki akses ke kode sumber Anda. Mereka bahkan membuat ini satu-liner di JDK 8 dengan Objects.requireNonNull(object, message)metode utilitas.
Robin
3
GAGAL harus dekat GAGAL. "Gagal Cepat" lebih dari sekadar aturan praktis. Kapan Anda tidak menginginkan perilaku ini? Perilaku lain apa pun berarti Anda menyembunyikan kesalahan. Ada "FAULTS" dan "FAILURES". KEGAGALAN adalah ketika program ini mencerna pointer NULL dan crash. Tapi baris kode itu bukan tempat FAULT berada. NULL berasal dari suatu tempat - argumen metode. Siapa yang melewati argumen itu? Dari beberapa baris kode referensi variabel lokal. Di mana itu ... Lihat? Itu menyebalkan. Tanggung jawab siapakah yang seharusnya melihat nilai buruk disimpan? Program Anda seharusnya macet saat itu.
Noah Spurrier
4
@ Thomas Poin bagus. Shmosel: Poin Thomas mungkin tersirat di titik gagal-cepat, tapi agak terkubur. Ini adalah konsep yang cukup penting sehingga memiliki nama sendiri: atomicity kegagalan . Lihat Bloch, Java Efektif , Butir 46. Ini memiliki semantik yang lebih kuat daripada gagal-cepat. Saya sarankan memanggilnya di titik yang terpisah. Jawabannya bagus sekali, BTW. +1
Stuart Marks
40

Ini untuk kejelasan, konsistensi, dan untuk mencegah pekerjaan tambahan yang tidak perlu dilakukan.

Pertimbangkan apa yang akan terjadi jika tidak ada klausa penjaga di bagian atas metode ini. Itu akan selalu memanggil hash(key)dan getNode(hash, key)bahkan ketika nulltelah dilewati untuk remappingFunctionsebelum NPE dilemparkan.

Lebih buruk lagi, jika ifkondisinya falsemaka kita mengambil elsecabang, yang tidak menggunakan remappingFunctionsama sekali, yang berarti metode tidak selalu membuang NPE ketika a nulldilewatkan; apakah itu tergantung pada kondisi peta.

Kedua skenario itu buruk. Jika nullbukan nilai yang valid untuk remappingFunctionmetode ini harus secara konsisten melempar pengecualian terlepas dari keadaan internal objek pada saat panggilan, dan harus melakukannya tanpa melakukan pekerjaan yang tidak perlu yang tidak ada gunanya mengingat bahwa itu hanya akan melempar. Akhirnya, itu adalah prinsip yang baik dari kode yang bersih dan jelas untuk memiliki penjaga di depan sehingga siapa pun yang meninjau kode sumber dapat dengan mudah melihat bahwa itu akan melakukannya.

Bahkan jika pengecualian saat ini dilemparkan oleh setiap cabang kode, ada kemungkinan bahwa revisi kode di masa depan akan mengubahnya. Melakukan pemeriksaan di awal memastikan pasti akan dilakukan.

David Conrad
sumber
25

Selain alasan yang tercantum oleh jawaban sempurna @ shmosel ...

Kinerja: Mungkin ada / telah ada manfaat kinerja (pada beberapa JVM) untuk melempar NPE secara eksplisit daripada membiarkan JVM melakukannya.

Itu tergantung pada strategi yang Java interpreter dan JIT compiler ambil untuk mendeteksi dereferencing dari null pointer. Salah satu strategi adalah untuk tidak menguji null, tetapi malah menjebak SIGSEGV yang terjadi ketika instruksi mencoba mengakses alamat 0. Ini adalah pendekatan tercepat dalam kasus di mana referensi selalu valid, tetapi mahal dalam kasus NPE.

Tes eksplisit untuk nulldalam kode akan menghindari kinerja SIGSEGV hit dalam skenario di mana NPE sering terjadi.

(Saya ragu bahwa ini akan menjadi optimasi mikro yang bermanfaat dalam JVM modern, tetapi bisa saja di masa lalu.)


Kompatibilitas: Kemungkinan alasan tidak ada pesan dalam pengecualian adalah untuk kompatibilitas dengan NPE yang dilemparkan oleh JVM itu sendiri. Dalam implementasi Java yang patuh, NPE yang dilempar oleh JVM memiliki nullpesan. (Android Java berbeda.)

Stephen C
sumber
20

Terlepas dari apa yang orang lain tunjukkan, ada baiknya memperhatikan peran kebaktian di sini. Dalam C #, misalnya, Anda juga memiliki konvensi yang sama untuk secara eksplisit meningkatkan pengecualian dalam kasus-kasus seperti ini, tetapi secara khusus ArgumentNullException, yang agak lebih spesifik. (C # konvensi adalah bahwa NullReferenceException selalu merupakan bug dari beberapa jenis - cukup sederhana, seharusnya tidak pernah terjadi dalam kode produksi; diberikan, ArgumentNullExceptionbiasanya tidak juga, tetapi bisa menjadi bug lebih sepanjang garis "Anda tidak mengerti bagaimana menggunakan perpustakaan dengan benar "jenis bug).

Jadi, pada dasarnya, dalam C # NullReferenceExceptionberarti bahwa program Anda benar-benar mencoba menggunakannya, sedangkan ArgumentNullExceptionitu berarti bahwa ia mengakui bahwa nilainya salah dan bahkan tidak repot-repot untuk mencoba menggunakannya. Implikasinya sebenarnya bisa berbeda (tergantung pada keadaan) karena ArgumentNullExceptionberarti bahwa metode yang dimaksud belum memiliki efek samping (karena gagal dalam prasyarat metode).

Secara kebetulan, jika Anda menaikkan sesuatu seperti ArgumentNullExceptionatau IllegalArgumentException, itulah bagian dari titik melakukan pemeriksaan: Anda menginginkan pengecualian yang berbeda dari yang biasanya Anda dapatkan.

Either way, secara eksplisit meningkatkan pengecualian memperkuat praktik baik menjadi eksplisit tentang pra-kondisi metode Anda dan argumen yang diharapkan, yang membuat kode lebih mudah dibaca, digunakan, dan dipelihara. Jika Anda tidak secara eksplisit memeriksa null, saya tidak tahu apakah itu karena Anda berpikir bahwa tidak ada yang akan pernah melewati nullargumen, Anda tetap menghitungnya untuk membuang pengecualian, atau Anda hanya lupa untuk memeriksa itu.

EJoshuaS - Pasang kembali Monica
sumber
4
+1 untuk paragraf tengah. Saya berpendapat bahwa kode tersebut harus 'membuang IllegalArgumentException baru ("remappingFunction tidak boleh nol");' Dengan begitu langsung terlihat apa yang salah. NPE yang ditampilkan agak ambigu.
Chris Parker
1
@ChrisParker Saya dulu pendapat yang sama, tetapi ternyata NullPointerException dimaksudkan untuk menandakan argumen nol yang diteruskan ke metode yang mengharapkan argumen non-nol, selain sebagai tanggapan runtime untuk upaya dereferensi null. Dari javadoc: "Aplikasi harus melempar instance kelas ini untuk menunjukkan penggunaan ilegal nullobjek lainnya." Saya tidak tergila-gila dengan itu, tapi itu tampaknya desain yang dimaksudkan.
VGR
1
Saya setuju, @ChrisParker - Saya pikir pengecualian itu lebih spesifik (karena kodenya bahkan tidak pernah mencoba melakukan apa pun dengan nilai tersebut, ia langsung mengenali bahwa ia seharusnya tidak menggunakannya). Saya suka konvensi C # dalam kasus ini. Konvensi C # adalah bahwa NullReferenceException(setara dengan NullPointerException) berarti bahwa kode Anda benar-benar mencoba menggunakannya (yang selalu merupakan bug - seharusnya tidak pernah terjadi dalam kode produksi), vs. "Saya tahu argumennya salah, jadi saya bahkan tidak coba gunakan itu. " Ada juga ArgumentException(yang berarti bahwa argumen itu salah karena alasan lain).
EJoshuaS
2
Saya akan mengatakan ini banyak, saya SELALU melempar IllegalArgumentException seperti yang dijelaskan. Saya selalu merasa nyaman melanggar konvensi ketika saya merasa konvensi itu bodoh.
Chris Parker
1
@PieterGeerkens - yeah, karena NullPointerException baris 35 jauh lebih baik daripada IllegalArgumentException ("Fungsi tidak bisa nol") baris 35. Serius?
Chris Parker
12

Sehingga Anda akan mendapatkan pengecualian begitu Anda melakukan kesalahan, daripada nanti ketika Anda menggunakan peta dan tidak akan mengerti mengapa itu terjadi.

Marquis dari Lorne
sumber
9

Ini mengubah kondisi kesalahan yang tampaknya tidak menentu menjadi pelanggaran kontrak yang jelas: Fungsi ini memiliki beberapa prasyarat untuk bekerja dengan benar, sehingga memeriksa mereka sebelumnya, memaksa mereka untuk dipenuhi.

Efeknya adalah, Anda tidak perlu melakukan debug computeIfPresent()ketika Anda mendapatkan pengecualian darinya. Setelah Anda melihat bahwa pengecualian tersebut berasal dari pemeriksaan prasyarat, Anda tahu bahwa Anda memanggil fungsi dengan argumen ilegal. Jika cek tidak ada di sana, Anda harus mengecualikan kemungkinan ada beberapa bug di dalamnya computeIfPresent()yang mengarah ke pengecualian yang dilemparkan.

Jelas, melempar NullPointerExceptionobat generik adalah pilihan yang sangat buruk, karena tidak menandakan adanya pelanggaran kontrak. IllegalArgumentExceptionakan menjadi pilihan yang lebih baik.


Sidenote:
Saya tidak tahu apakah Java mengizinkan ini (saya ragu), tetapi programmer C / C ++ menggunakan assert()dalam kasus ini, yang secara signifikan lebih baik untuk debugging: Ini memberitahu program untuk crash segera dan sekeras mungkin harus disediakan kondisi bernilai false. Jadi, jika Anda berlari

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

di bawah debugger, dan sesuatu masuk NULLke salah satu argumen, program akan berhenti tepat di baris yang mengatakan argumen mana NULL, dan Anda akan dapat memeriksa semua variabel lokal dari seluruh tumpukan panggilan di waktu luang.

cmaster - mengembalikan monica
sumber
1
assert something != null;tapi itu membutuhkan -assertionsbendera saat menjalankan aplikasi. Jika -assertionsbendera tidak ada di sana, kata kunci yang menegaskan tidak akan membuang AssertionException
Zoe
Saya setuju, itu sebabnya saya lebih suka konvensi C # di sini - referensi nol, argumen tidak valid, dan argumen nol semua umumnya menyiratkan bug dari beberapa jenis, tetapi mereka menyiratkan berbagai jenis bug. "Anda mencoba menggunakan referensi nol" seringkali sangat berbeda dari "Anda menyalahgunakan perpustakaan."
EJoshuaS
7

Itu karena itu mungkin tidak terjadi secara alami. Mari kita lihat sepotong kode seperti ini:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(tentu saja ada banyak masalah dalam sampel ini tentang kualitas kode. Maaf malas membayangkan sampel sempurna)

Situasi ketika ctidak akan benar-benar digunakan dan NullPointerExceptiontidak akan dibuang bahkan jika c == nullmemungkinkan.

Dalam situasi yang lebih rumit, menjadi sangat tidak mudah untuk memburu kasus seperti itu. Inilah sebabnya mengapa pemeriksaan umum seperti if (c == null) throw new NullPointerException()lebih baik.

Arenim
sumber
Dapat diperdebatkan, sepotong kode yang berfungsi tanpa koneksi basis data ketika tidak benar-benar dibutuhkan adalah hal yang baik, dan kode yang terhubung ke database hanya untuk melihat apakah ia dapat gagal melakukannya biasanya cukup menjengkelkan.
Dmitry Grigoryev
5

Dimaksudkan untuk melindungi kerusakan lebih lanjut, atau memasuki kondisi yang tidak konsisten.

Fairoz
sumber
1

Terlepas dari semua jawaban bagus lainnya di sini, saya juga ingin menambahkan beberapa kasus.

Anda dapat menambahkan pesan jika Anda membuat pengecualian Anda sendiri

Jika Anda melempar sendiri, NullPointerExceptionAnda dapat menambahkan pesan (yang memang seharusnya!)

Pesan default adalah nulldari new NullPointerException()dan semua metode yang menggunakannya, misalnya Objects.requireNonNull. Jika Anda mencetak nol itu bahkan dapat menerjemahkan ke string kosong ...

Agak pendek dan tidak informatif ...

Jejak tumpukan akan memberikan banyak informasi, tetapi bagi pengguna untuk mengetahui apa yang nol mereka harus menggali kode dan melihat baris yang tepat.

Sekarang bayangkan NPE dibungkus dan dikirim melalui internet, misalnya sebagai pesan dalam kesalahan layanan web, mungkin antara departemen yang berbeda atau bahkan organisasi. Skenario terburuk, tidak ada yang tahu apa nullkepanjangan dari ...

Panggilan metode berantai akan membuat Anda menebak

Pengecualian hanya akan memberi tahu Anda pada baris apa pengecualian itu terjadi. Pertimbangkan baris berikut:

repository.getService(someObject.someMethod());

Jika Anda mendapatkan NPE dan menunjuk pada baris ini, yang mana dari repositorydan someObjectnol?

Sebaliknya, memeriksa variabel-variabel ini ketika Anda mendapatkannya setidaknya akan menunjuk ke baris di mana mereka diharapkan satu-satunya variabel yang ditangani. Dan, seperti yang disebutkan sebelumnya, bahkan lebih baik jika pesan kesalahan Anda berisi nama variabel atau sejenisnya.

Kesalahan saat memproses banyak input harus memberikan informasi pengenal

Bayangkan program Anda sedang memproses file input dengan ribuan baris dan tiba-tiba ada NullPointerException. Anda melihat tempat itu dan menyadari beberapa input salah ... input apa? Anda akan memerlukan informasi lebih lanjut tentang nomor baris, mungkin kolom atau bahkan seluruh teks baris untuk memahami baris apa dalam file yang perlu diperbaiki.

Erk
sumber