Učinkovito rukovanje Java NullPointerException

Ne treba puno iskustva u razvoju Jave da biste iz prve ruke naučili o čemu je NullPointerException. U stvari, jedna je osoba istaknula kako se baviti ovim problemom kao najvećom pogreškom Java programera. Prethodno sam blogovao o upotrebi String.value (Object) za smanjenje neželjenih NullPointerExceptions. Postoji nekoliko drugih jednostavnih tehnika koje se mogu koristiti za smanjenje ili uklanjanje pojava ove uobičajene vrste RuntimeException koja je s nama od JDK 1.0. Ovaj post na blogu prikuplja i sažima neke od najpopularnijih ovih tehnika.

Prije upotrebe provjerite ima li svaki objekt nulu

Najsigurniji način da se izbjegne NullPointerException je provjera svih referenci na objekt kako bi se osiguralo da nisu nule prije pristupa jednom od polja ili metoda objekta. Kao što slijedeći primjer pokazuje, ovo je vrlo jednostavna tehnika.

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

U gornjem kodu, upotrijebljeni Deque namjerno se inicijalizira tako da nuli olakšava primjer. Kôd u prvom trybloku ne provjerava ima li nulu prije pokušaja pristupa Deque metodi. Kôd u drugom trybloku provjerava ima li nulu i instancira implementaciju Deque(LinkedList) ako je null. Izlaz iz oba primjera izgleda ovako:

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) 

Poruka koja slijedi GREŠKA u gornjem izlazu ukazuje na to da NullPointerExceptionse baca a kada se pokuša pokušaj poziva nule Deque. Poruka koja slijedi INFO u gornjem izlazu ukazuje Dequena to da je iznimkom izbjegnuta prvo provjeravanjem nule, a zatim instanciranjem nove implementacije za nju kada je null.

Ovaj se pristup često koristi i, kao što je gore prikazano, može biti vrlo koristan u izbjegavanju neželjenih (neočekivanih) NullPointerExceptionslučajeva. Međutim, nije bez troškova. Provjera nule prije upotrebe svakog objekta može napuhati kôd, može biti zamorno pisati i otvara više prostora za probleme s razvojem i održavanjem dodatnog koda. Iz tog razloga govori se o uvođenju podrške za Java jezik za ugrađenu null detekciju, automatskom dodavanju ovih provjera nule nakon početnog kodiranja, nulto sigurnim vrstama, upotrebi Aspektno orijentiranog programiranja (AOP) za dodavanje null provjere za byte kôd i druge alate za otkrivanje nule.

Groovy već nudi prikladan mehanizam za postupanje s referencama na objekte koje su potencijalno nevaljane. Groovyjev operator sigurne navigacije ( ?.) vraća nulu, a ne baca NullPointerExceptionkada se pristupa referenci null objekta.

Budući da provjera nule za svaku referencu objekta može biti zamorna i napuhati kôd, mnogi programeri odlučuju razumno odabrati koje će objekte provjeriti da li su nuli. To obično dovodi do provjere nule na svim objektima potencijalno nepoznatog podrijetla. Ideja je ovdje da se objekti mogu provjeriti na izloženim sučeljima i nakon toga pretpostaviti da su sigurni nakon početne provjere.

Ovo je situacija u kojoj ternarni operator može biti posebno koristan. Umjesto

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

ternarni operator podržava ovu sažetiju sintaksu

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

Provjerite argumente metode za null

Tehnika o kojoj smo upravo razgovarali može se koristiti na svim objektima. Kao što je navedeno u opisu te tehnike, mnogi programeri odlučuju provjeravati nulu objekata samo ako dolaze iz "nepouzdanih" izvora. To često znači prvo testiranje na nulu u metodama izloženim vanjskim pozivima. Na primjer, u određenoj klasi programer može odabrati provjeriti nulu na svim objektima proslijeđenim publicmetodama, ali ne i nulu u privatemetodama.

Sljedeći kod pokazuje ovu provjeru nule pri unosu metode. Uključuje jednu metodu kao demonstrativnu metodu koja se okreće i poziva dvije metode, prosljeđujući svakoj metodi jedan null argument. Jedna od metoda koja prima null argument prvo provjerava taj argument null, ali druga samo pretpostavlja da prosljeđeni parametar nije 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); } } 

Kada se izvrši gornji kod, izlaz se pojavljuje kao što je prikazano sljedeće.

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. 

U oba slučaja zabilježena je poruka pogreške. Međutim, slučaj u kojem je provjereno ima li nulu bacio je oglašeni IllegalArgumentException koji je sadržavao dodatne informacije o kontekstu kada je došlo do nule. Alternativno, s tim se nulskim parametrom moglo postupati na razne načine. Za slučaj u kojem nije obrađen null parametar, nije bilo mogućnosti za postupanje s njim. Mnogi ljudi radije NullPolinterExceptiondodaju dodatne informacije o kontekstu kada je nula eksplicitno otkrivena (vidi stavku br. 60 u drugom izdanju efektivne Jave ili stavku br. 42 u prvom izdanju), ali imam malu prednost IllegalArgumentExceptionkada je to izričito Argument metode koji je null, jer mislim da sama iznimka dodaje detalje konteksta i lako je uključiti "null" u temu.

Tehnika provjere argumenata metode za null doista je podskup općenitije tehnike provjere nule za sve objekte. Međutim, kao što je gore navedeno, argumentima javno izloženih metoda često se najmanje vjeruje u aplikaciji, pa je njihova provjera možda važnija od provjere nule prosječnog objekta.

Provjera parametara metode za nulu također je podskup općenitije prakse provjere parametara metode za opću valjanost, kao što je objašnjeno u stavci br. 38 drugog izdanja učinkovite Jave (stavka 23 u prvom izdanju).

Razmotrite primitivce, a ne predmete

Mislim da nije dobra ideja odabrati primitivni tip podataka (kao što je int) umjesto odgovarajućeg referentnog tipa objekta (kao što je Integer) samo da bi se izbjegla mogućnost a NullPointerException, ali ne može se poreći da je jedna od prednosti primitivni tipovi je da ne vode do NullPointerExceptions. Međutim, primitivce još uvijek treba provjeriti na valjanost (mjesec dana ne može biti negativan cijeli broj), tako da ta korist može biti mala. S druge strane, primitivi se ne mogu koristiti u kolekcijama Java i ponekad se dogodi da osoba želi postaviti vrijednost na nulu.

Najvažnije je biti vrlo oprezan u vezi s kombinacijom primitiva, referentnih tipova i autoboksa. U Efektivnoj Javi (drugo izdanje, stavka # 49.) Nalazi se upozorenje u vezi s opasnostima, uključujući bacanje NullPointerException, koje se odnose na neoprezno miješanje primitivnih i referentnih vrsta.

Pažljivo razmotrite pozive lančanim metodama

A NullPointerExceptionje vrlo jednostavno pronaći, jer će u retku biti navedeno gdje se to dogodilo. Na primjer, trag stoga može izgledati kao sljedeći:

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

Trag steka čini očitim da NullPointerExceptionje bačen kao rezultat koda izvedenog na retku 222 od AvoidingNullPointerExamples.java. Čak i ako je naveden broj retka, još uvijek može biti teško suziti koji je objekt nulan ako na istoj liniji ima više objekata s metodama ili poljima.

Na primjer, izraz kao što someObject.getObjectA().getObjectB().getObjectC().toString();ima četiri moguća poziva koji bi NullPointerExceptionatribut mogli odbaciti u isti redak koda. Upotreba programa za otklanjanje pogrešaka može vam pomoći u tome, ali može biti situacija kada je poželjno jednostavno razbiti gornji kôd tako da se svaki poziv izvodi na zasebnoj liniji. To omogućuje broju retka sadržanom u tragu steka da lako ukaže u kojem je točno pozivu bio problem. Nadalje, olakšava eksplicitnu provjeru nule svakog objekta. Međutim, s negativne strane, razbijanje koda povećava redak broja kodova (nekima je to pozitivno!) I možda neće uvijek biti poželjno, pogotovo ako je sigurno da niti jedna od metoda u pitanju nikada neće biti ništavna.

Neka NullPointerExceptions budu informativniji

U gornjoj preporuci upozorenje je trebalo pažljivo razmotriti uporabu lanca poziva metode, prvenstveno zato što je to što je broj retka u tragu steka NullPointerExceptionmanje koristan nego što bi inače mogao biti. Međutim, broj retka prikazuje se u tragu steka samo kad je kod sastavljen s uključenom zastavicom za otklanjanje pogrešaka. Ako je kompiliran bez otklanjanja pogrešaka, trag steka izgleda kao sljedeći:

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

Kao što gornji izlaz pokazuje, postoji naziv metode, ali ne i broj retka za NullPointerException. To otežava odmah prepoznavanje onoga što je u kodu dovelo do iznimke. Jedan od načina da se to riješi je pružanje kontekstnih informacija u bilo kojem dobačenom obliku NullPointerException. Ova ideja je demonstrirana ranije kada je NullPointerExceptiona uhvaćen i ponovno bačen s dodatnim informacijama o kontekstu kao a IllegalArgumentException. Međutim, čak i ako se iznimka jednostavno ponovno postavi kao druga NullPointerExceptions informacijama o kontekstu, ona je i dalje korisna. Informacije o kontekstu pomažu osobi koja otklanja pogreške u kodu da brže prepozna pravi uzrok problema.

Sljedeći primjer pokazuje ovo načelo.

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

Izlaz iz izvođenja gornjeg koda izgleda kako slijedi.

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