Penanganan Java NullPointerException yang Efektif

Tidak perlu banyak pengalaman pengembangan Java untuk mempelajari secara langsung apa itu NullPointerException. Faktanya, satu orang telah menyoroti menangani ini sebagai kesalahan nomor satu yang dibuat oleh pengembang Java. Saya membuat blog sebelumnya tentang penggunaan String.value (Object) untuk mengurangi NullPointerExceptions yang tidak diinginkan. Ada beberapa teknik sederhana lain yang dapat digunakan untuk mengurangi atau menghilangkan kemunculan jenis umum RuntimeException yang telah ada sejak JDK 1.0. Posting blog ini mengumpulkan dan merangkum beberapa teknik yang paling populer.

Periksa Setiap Objek Untuk Null Sebelum Menggunakan

Cara paling pasti untuk menghindari NullPointerException adalah dengan memeriksa semua referensi objek untuk memastikan bahwa referensi tersebut bukan null sebelum mengakses salah satu bidang atau metode objek. Seperti yang ditunjukkan contoh berikut, ini adalah teknik yang sangat sederhana.

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); } 

Pada kode di atas, Deque yang digunakan sengaja diinisialisasi ke null untuk memfasilitasi contoh. Kode di tryblok pertama tidak memeriksa null sebelum mencoba mengakses metode Deque. Kode di tryblok kedua memeriksa null dan membuat instance implementasi Deque(LinkedList) jika nilainya null. Output dari kedua contoh tersebut terlihat 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) 

Pesan setelah ERROR dalam output di atas menunjukkan bahwa a NullPointerExceptiondilemparkan ketika panggilan metode dicoba pada null Deque. Pesan yang mengikuti INFO pada output di atas menunjukkan bahwa dengan memeriksa Dequenull terlebih dahulu dan kemudian membuat instance implementasi baru ketika nilainya null, pengecualian tersebut dihindari sama sekali.

Pendekatan ini sering digunakan dan, seperti yang ditunjukkan di atas, bisa sangat berguna dalam menghindari NullPointerExceptionkejadian yang tidak diinginkan (tidak terduga) . Namun, ini bukannya tanpa biaya. Memeriksa null sebelum menggunakan setiap objek dapat membengkakkan kode, membosankan untuk ditulis, dan membuka lebih banyak ruang untuk masalah dengan pengembangan dan pemeliharaan kode tambahan. Untuk alasan ini, telah ada pembicaraan tentang memperkenalkan dukungan bahasa Java untuk deteksi null built-in, otomatis menambahkan pemeriksaan ini untuk null setelah pengkodean awal, tipe null-safe, penggunaan Aspect-Oriented Programming (AOP) untuk menambahkan pemeriksaan null ke kode byte, dan alat deteksi null lainnya.

Groovy sudah menyediakan mekanisme yang nyaman untuk menangani referensi objek yang berpotensi null. Operator navigasi aman Groovy ( ?.) mengembalikan null daripada membuang NullPointerExceptionsaat referensi objek null diakses.

Karena memeriksa null untuk setiap referensi objek bisa membosankan dan membengkakkan kode, banyak developer memilih untuk memilih dengan bijaksana objek mana yang akan diperiksa null. Ini biasanya mengarah pada pemeriksaan null pada semua objek yang kemungkinan asalnya tidak diketahui. Idenya di sini adalah bahwa objek dapat diperiksa pada antarmuka yang terbuka dan kemudian dianggap aman setelah pemeriksaan awal.

Ini adalah situasi di mana operator terner dapat sangat berguna. Dari pada

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

operator terner mendukung sintaks yang lebih ringkas ini

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

Periksa Argumen Metode untuk Null

Teknik yang baru saja didiskusikan dapat digunakan pada semua objek. Seperti yang dinyatakan dalam deskripsi teknik itu, banyak pengembang memilih untuk hanya memeriksa objek untuk null ketika mereka berasal dari sumber "tidak tepercaya". Ini sering berarti menguji null terlebih dahulu dalam metode yang diekspos ke pemanggil eksternal. Misalnya, di kelas tertentu, pengembang mungkin memilih untuk memeriksa null pada semua objek yang diteruskan ke publicmetode, tetapi tidak memeriksa null di privatemetode.

Kode berikut menunjukkan ini memeriksa null pada entri metode. Ini mencakup satu metode sebagai metode demonstratif yang berbalik dan memanggil dua metode, meneruskan setiap metode satu argumen nol. Salah satu metode yang menerima argumen null memeriksa argumen itu terlebih dahulu, tetapi yang lain hanya mengasumsikan parameter yang diteruskan bukan null.

 /** * 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); } } 

Ketika kode di atas dijalankan, output muncul seperti gambar berikut.

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 kasus tersebut, pesan kesalahan dicatat. Namun, kasus di mana sebuah null diperiksa untuk memunculkan IllegalArgumentException yang diiklankan yang menyertakan informasi konteks tambahan tentang kapan null ditemukan. Alternatifnya, parameter null ini dapat ditangani dengan berbagai cara. Untuk kasus di mana parameter null tidak ditangani, tidak ada opsi cara menanganinya. Banyak orang lebih suka melempar NullPolinterExceptiondengan informasi konteks tambahan ketika sebuah null secara eksplisit ditemukan (lihat Item # 60 di Edisi Kedua Java Efektif atau Item # 42 di Edisi Pertama), tetapi saya memiliki sedikit preferensi IllegalArgumentExceptionketika itu secara eksplisit a argumen metode yang null karena menurut saya pengecualian itu menambahkan detail konteks dan mudah untuk menyertakan "null" dalam subjek.

Teknik memeriksa argumen metode untuk null sebenarnya merupakan bagian dari teknik yang lebih umum untuk memeriksa semua objek untuk null. Namun, seperti diuraikan di atas, argumen untuk metode yang diekspos secara publik sering kali paling tidak dipercaya dalam sebuah aplikasi dan karenanya memeriksa mereka mungkin lebih penting daripada memeriksa objek rata-rata untuk null.

Memeriksa parameter metode untuk null juga merupakan bagian dari praktik yang lebih umum untuk memeriksa parameter metode untuk validitas umum seperti yang dibahas dalam Item # 38 dari Edisi Kedua Java Efektif (Item 23 di Edisi Pertama).

Pertimbangkan Primitif daripada Objek

Menurut saya bukan ide yang baik untuk memilih tipe data primitif (seperti int) di atas tipe referensi objek yang sesuai (seperti Integer) hanya untuk menghindari kemungkinan a NullPointerException, tetapi tidak dapat disangkal bahwa salah satu keuntungan dari tipe primitif adalah mereka tidak mengarah pada NullPointerExceptions. Namun, primitif masih harus diperiksa keabsahannya (satu bulan tidak boleh bilangan bulat negatif) dan manfaat ini mungkin kecil. Di sisi lain, primitif tidak dapat digunakan di Java Collections dan ada kalanya seseorang menginginkan kemampuan untuk menetapkan nilai ke null.

Yang paling penting adalah sangat berhati-hati tentang kombinasi primitif, jenis referensi, dan autoboxing. Ada peringatan di Efektif Java (Edisi Kedua, Item # 49) tentang bahaya, termasuk membuang NullPointerException, terkait dengan pencampuran jenis primitif dan referensi ceroboh.

Pertimbangkan dengan hati-hati Panggilan Metode Rantai

A NullPointerExceptionbisa sangat mudah ditemukan karena nomor baris akan menyatakan tempat terjadinya. Misalnya, pelacakan tumpukan mungkin terlihat seperti yang ditunjukkan berikut ini:

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

Jejak tumpukan memperjelas bahwa NullPointerExceptiondilempar sebagai hasil dari kode yang dieksekusi pada baris 222 dari AvoidingNullPointerExamples.java. Bahkan dengan nomor baris yang disediakan, masih sulit untuk mempersempit objek mana yang nol jika ada beberapa objek dengan metode atau bidang yang diakses pada baris yang sama.

Misalnya, pernyataan like someObject.getObjectA().getObjectB().getObjectC().toString();memiliki empat kemungkinan panggilan yang mungkin telah melontarkan NullPointerExceptionatribut ke baris kode yang sama. Menggunakan debugger dapat membantu dalam hal ini, tetapi mungkin ada situasi ketika lebih disukai untuk memecah kode di atas sehingga setiap panggilan dilakukan pada baris terpisah. Hal ini memungkinkan nomor baris yang terdapat dalam pelacakan tumpukan dengan mudah menunjukkan panggilan mana yang menjadi masalah. Selain itu, ini memfasilitasi pemeriksaan eksplisit setiap objek untuk null. Namun, pada sisi negatifnya, memecah kode meningkatkan jumlah baris kode (untuk beberapa yang positif!) Dan mungkin tidak selalu diinginkan, terutama jika seseorang yakin tidak ada metode yang dipermasalahkan akan pernah menjadi nol.

Jadikan NullPointerExceptions Lebih Informatif

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 keluaran di atas, ada nama metode, tetapi tidak ada nomor baris untuk NullPointerException. Ini membuatnya lebih sulit untuk segera mengidentifikasi apa yang ada dalam kode yang menyebabkan pengecualian. Salah satu cara untuk mengatasinya adalah dengan memberikan informasi konteks dalam bentuk apa pun NullPointerException. Ide ini telah didemonstrasikan sebelumnya ketika a NullPointerExceptionditangkap dan dilemparkan kembali dengan informasi konteks tambahan sebagai a IllegalArgumentException. Namun, meskipun pengecualian hanya dilemparkan kembali sebagai pengecualian lain NullPointerExceptiondengan informasi konteks, itu masih membantu. Informasi konteks membantu orang tersebut men-debug kode untuk lebih cepat mengidentifikasi penyebab sebenarnya dari masalah tersebut.

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); } 

Output dari menjalankan kode di atas terlihat sebagai 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