Pengendalian Java NullPointerException yang berkesan

Tidak memerlukan banyak pengalaman pengembangan Java untuk belajar secara langsung mengenai apa itu NullPointerException. Sebenarnya, satu orang telah menonjolkan menangani ini sebagai kesilapan nombor satu yang dilakukan oleh pembangun Java. Saya menulis blog sebelumnya mengenai penggunaan String.value (Object) untuk mengurangkan NullPointerExceptions yang tidak diingini. Terdapat beberapa teknik mudah lain yang dapat digunakan untuk mengurangkan atau menghilangkan kejadian jenis RuntimeException biasa yang telah ada bersama kami sejak JDK 1.0. Catatan blog ini mengumpulkan dan merangkum beberapa teknik yang paling popular.

Periksa Setiap Objek Null Sebelum Menggunakan

Cara yang paling pasti untuk mengelakkan NullPointerException adalah dengan memeriksa semua rujukan objek untuk memastikannya tidak kosong sebelum mengakses salah satu bidang atau kaedah objek. Seperti yang ditunjukkan oleh contoh berikut, ini adalah teknik yang sangat mudah.

final String causeStr = "adding String to Deque that is set to null."; final String elementStr = "Fudd"; Deque deque = null; try { deque.push(elementStr); log("Successful at " + causeStr, System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { if (deque == null) { deque = new LinkedList(); } deque.push(elementStr); log( "Successful at " + causeStr + " (by checking first for null and instantiating Deque implementation)", System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } 

Dalam kod di atas, Deque yang digunakan sengaja dimulakan untuk dibatalkan untuk memudahkan contohnya. Kod di tryblok pertama tidak memeriksa nol sebelum cuba mengakses kaedah Deque. Kod di tryblok kedua memeriksa nol dan menunjukkan pelaksanaan Deque(LinkedList) jika tidak ada. Hasil daripada kedua-dua contoh kelihatan seperti ini:

ERROR: NullPointerException encountered while trying to adding String to Deque that is set to null. java.lang.NullPointerException INFO: Successful at adding String to Deque that is set to null. (by checking first for null and instantiating Deque implementation) 

Mesej berikut KESALAHAN dalam output di atas menunjukkan bahawa a NullPointerExceptiondilemparkan ketika panggilan kaedah dicuba pada nol Deque. Mesej berikut INFO dalam output di atas menunjukkan bahawa dengan memeriksa Dequenol terlebih dahulu dan kemudian menunjukkan implementasi baru untuknya ketika nol, pengecualian dihindari sama sekali.

Pendekatan ini sering digunakan dan, seperti yang ditunjukkan di atas, sangat berguna dalam mengelakkan NullPointerExceptionkejadian yang tidak diingini (tidak dijangka) . Walau bagaimanapun, ia bukan tanpa kosnya. Memeriksa nol sebelum menggunakan setiap objek dapat mengaburkan kod, dapat membosankan untuk menulis, dan membuka lebih banyak ruang untuk masalah dengan pengembangan dan pemeliharaan kod tambahan. Atas sebab ini, ada pembicaraan untuk memperkenalkan sokongan bahasa Java untuk pengesanan nol bawaan, penambahan automatik pemeriksaan ini untuk nol setelah pengekodan awal, jenis aman null, penggunaan Pengaturcaraan Berorientasi Aspek (AOP) untuk menambahkan pemeriksaan nol ke kod bait, dan alat pengesanan nol lain.

Groovy sudah menyediakan mekanisme yang mudah untuk menangani rujukan objek yang berpotensi nol. Pengendali navigasi selamat Groovy ( ?.) mengembalikan null daripada membuang NullPointerExceptionketika rujukan objek null diakses.

Kerana memeriksa nol untuk setiap rujukan objek boleh membosankan dan tidak mengagumkan kodnya, banyak pembangun memilih untuk memilih objek mana yang harus diperiksa nol. Ini biasanya membawa kepada pemeriksaan nol pada semua objek yang berpotensi tidak diketahui. Ideanya di sini adalah bahawa objek dapat diperiksa pada antara muka yang terdedah dan kemudian dianggap selamat setelah pemeriksaan awal.

Ini adalah keadaan di mana pengendali ternary sangat berguna. Bukannya

// retrieved a BigDecimal called someObject String returnString; if (someObject != null) { returnString = someObject.toEngineeringString(); } else { returnString = ""; } 

pengendali ternary menyokong sintaks yang lebih ringkas ini

// retrieved a BigDecimal called someObject final String returnString = (someObject != null) ? someObject.toEngineeringString() : ""; } 

Semak Kaedah Berhujah untuk Null

Teknik yang baru dibincangkan boleh digunakan pada semua objek. Seperti yang dinyatakan dalam keterangan teknik itu, banyak pembangun memilih untuk memeriksa objek hanya nol ketika berasal dari sumber "tidak dipercayai". Ini sering kali bermaksud menguji perkara pertama yang tidak sah dalam kaedah yang terdedah kepada pemanggil luar. Sebagai contoh, dalam kelas tertentu, pembangun mungkin memilih untuk memeriksa nol pada semua objek yang diteruskan ke publickaedah, tetapi tidak memeriksa privatekaedah nol .

Kod berikut menunjukkan pemeriksaan ini untuk entri kaedah null on. Ini merangkumi kaedah tunggal sebagai kaedah demonstrasi yang berbalik dan memanggil dua kaedah, melewati setiap kaedah sebagai argumen nol tunggal. Salah satu kaedah untuk menerima argumen nol memeriksa argumen itu untuk nol terlebih dahulu, tetapi yang lain hanya menganggap parameter yang dilalui tidak nol.

 /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. * @throws IllegalArgumentException Thrown if the provided StringBuilder is * null. */ private void appendPredefinedTextToProvidedBuilderCheckForNull( final StringBuilder builder) { if (builder == null) { throw new IllegalArgumentException( "The provided StringBuilder was null; non-null value must be provided."); } builder.append("Thanks for supplying a StringBuilder."); } /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. */ private void appendPredefinedTextToProvidedBuilderNoCheckForNull( final StringBuilder builder) { builder.append("Thanks for supplying a StringBuilder."); } /** * Demonstrate effect of checking parameters for null before trying to use * passed-in parameters that are potentially null. */ public void demonstrateCheckingArgumentsForNull() { final String causeStr = "provide null to method as argument."; logHeader("DEMONSTRATING CHECKING METHOD PARAMETERS FOR NULL", System.out); try { appendPredefinedTextToProvidedBuilderNoCheckForNull(null); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { appendPredefinedTextToProvidedBuilderCheckForNull(null); } catch (IllegalArgumentException illegalArgument) { log(causeStr, illegalArgument, System.out); } } 

Apabila kod di atas dijalankan, output akan muncul seperti yang ditunjukkan seterusnya.

ERROR: NullPointerException encountered while trying to provide null to method as argument. java.lang.NullPointerException ERROR: IllegalArgumentException encountered while trying to provide null to method as argument. java.lang.IllegalArgumentException: The provided StringBuilder was null; non-null value must be provided. 

Dalam kedua kes tersebut, mesej ralat telah dicatat. Walau bagaimanapun, kes di mana nol diperiksa untuk melemparkan IllegalArgumentException yang diiklankan yang merangkumi maklumat konteks tambahan mengenai kapan nol itu ditemui. Sebagai alternatif, parameter nol ini dapat ditangani dengan berbagai cara. Untuk kes di mana parameter nol tidak ditangani, tidak ada pilihan untuk menanganinya. Ramai orang lebih suka membuang NullPolinterExceptiondengan maklumat konteks tambahan apabila nol dijumpai secara eksplisit (lihat Item # 60 di Edisi Kedua Java Berkesan atau Item # 42 dalam Edisi Pertama), tetapi saya mempunyai sedikit pilihan IllegalArgumentExceptionketika kaedah kaedah yang tidak sah kerana saya rasa pengecualiannya menambah perincian konteks dan mudah memasukkan "null" dalam subjek.

Teknik menyemak hujah kaedah untuk null benar-benar merupakan sebahagian daripada teknik yang lebih umum untuk memeriksa semua objek untuk null. Namun, seperti yang dijelaskan di atas, argumen untuk kaedah yang didedahkan kepada umum seringkali paling tidak dapat dipercaya dalam aplikasi dan oleh itu, memeriksanya mungkin lebih penting daripada memeriksa rata-rata objek untuk nol.

Memeriksa parameter kaedah untuk null juga merupakan subkumpulan amalan kaedah pemeriksaan umum yang lebih umum untuk kesahan umum seperti yang dibincangkan dalam Item # 38 Edisi Kedua Java Berkesan (Item 23 dalam Edisi Pertama).

Pertimbangkan Primitif dan Bukan Objek

Saya rasa bukan idea yang baik untuk memilih jenis data primitif (seperti int) berbanding jenis rujukan objek yang sesuai (seperti Integer) hanya untuk mengelakkan kemungkinan adanya NullPointerException, tetapi tidak dapat dinafikan bahawa salah satu kelebihan jenis primitif adalah bahawa ia tidak membawa kepada NullPointerExceptions. Walau bagaimanapun, primitif masih harus diperiksa untuk kesahannya (sebulan tidak boleh menjadi bilangan bulat negatif) dan jadi manfaat ini mungkin kecil. Sebaliknya, primitif tidak dapat digunakan dalam Koleksi Java dan ada kalanya seseorang menginginkan kemampuan untuk menetapkan nilai menjadi nol.

Perkara yang paling penting adalah sangat berhati-hati dengan gabungan primitif, jenis rujukan, dan autoboxing. Terdapat peringatan di Java Berkesan (Edisi Kedua, Item # 49) mengenai bahaya, termasuk melemparkan NullPointerException, yang berkaitan dengan pencampuran jenis primitif dan rujukan yang ceroboh.

Pertimbangkan Panggilan Kaedah Berantai dengan berhati-hati

A NullPointerExceptionsangat mudah dicari kerana nombor garis akan menyatakan di mana ia berlaku. Contohnya, jejak timbunan mungkin kelihatan seperti yang ditunjukkan seterusnya:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(AvoidingNullPointerExamples.java:222) at dustin.examples.AvoidingNullPointerExamples.main(AvoidingNullPointerExamples.java:247) 

Jejak timbunan menjadikannya jelas bahawa NullPointerExceptiondilemparkan sebagai hasil kod yang dilaksanakan pada baris 222 dari AvoidingNullPointerExamples.java. Walaupun dengan nombor garis yang disediakan, masih sukar untuk menyempitkan objek mana yang nol jika terdapat banyak objek dengan kaedah atau bidang yang diakses pada baris yang sama.

Contohnya, pernyataan seperti someObject.getObjectA().getObjectB().getObjectC().toString();mempunyai empat kemungkinan panggilan yang mungkin NullPointerExceptionmenyebabkan hubungan dengan kod yang sama. Menggunakan debugger dapat membantu dengan ini, tetapi mungkin ada situasi di mana lebih baik hanya memecahkan kod di atas sehingga setiap panggilan dilakukan pada baris yang terpisah. Ini membolehkan nombor baris yang terdapat dalam timbunan jejak dengan mudah menunjukkan panggilan tepat yang menjadi masalah. Tambahan lagi, ini memudahkan pemeriksaan secara eksplisit setiap objek untuk nol. Walau bagaimanapun, pada sisi negatifnya, memecah kod akan meningkatkan jumlah kod kod (bagi beberapa yang positif!) Dan mungkin tidak selalu diinginkan, terutama jika salah satu kaedah pasti tidak akan menjadi batal.

Jadikan Pengecualian NullPointer Lebih Bermaklumat

In the above recommendation, the warning was to consider carefully use of method call chaining primarily because it made having the line number in the stack trace for a NullPointerException less helpful than it otherwise might be. However, the line number is only shown in a stack trace when the code was compiled with the debug flag turned on. If it was compiled without debug, the stack trace looks like that shown next:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(Unknown Source) at dustin.examples.AvoidingNullPointerExamples.main(Unknown Source) 

Seperti yang ditunjukkan oleh output di atas, ada nama kaedah, tetapi tidak ada nombor baris untuk NullPointerException. Ini menjadikannya lebih sukar untuk segera mengenal pasti apa yang terdapat dalam kod yang menyebabkan pengecualian. Salah satu cara untuk mengatasi ini adalah dengan memberikan maklumat konteks dalam apa pun yang dilemparkan NullPointerException. Idea ini ditunjukkan sebelum ini ketika NullPointerExceptionditangkap dan dilemparkan semula dengan maklumat konteks tambahan sebagai IllegalArgumentException. Walau bagaimanapun, walaupun pengecualian dilemparkan semula sebagai NullPointerExceptionmaklumat konteks yang lain, ia tetap berguna. Maklumat konteks membantu orang menyahpepijat kod untuk mengenal pasti punca sebenar masalah dengan lebih cepat.

Contoh berikut menunjukkan prinsip ini.

final Calendar nullCalendar = null; try { final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } try { if (nullCalendar == null) { throw new NullPointerException("Could not extract Date from provided Calendar"); } final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } 

Hasil daripada menjalankan kod di atas kelihatan seperti berikut.

ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException: Could not extract Date from provided Calendar