Effektiv hantering av Java NullPointerException

Det tar inte mycket Java-utvecklingserfarenhet för att lära sig vad NullPointerException handlar om. Faktum är att en person har markerat att hantera detta som det främsta misstaget Java-utvecklare gör. Jag bloggade tidigare om användning av String.value (Object) för att minska oönskade NullPointerExceptions. Det finns flera andra enkla tekniker man kan använda för att minska eller eliminera förekomsten av denna vanliga typ av RuntimeException som har funnits sedan JDK 1.0. Detta blogginlägg samlar och sammanfattar några av de mest populära av dessa tekniker.

Kontrollera att varje objekt är noll innan du använder det

Det säkraste sättet att undvika en NullPointerException är att kontrollera alla objektreferenser för att säkerställa att de inte är noll innan du öppnar något av objektets fält eller metoder. Som följande exempel indikerar är detta en mycket enkel teknik.

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

I koden ovan initialiseras avsiktligt Deque till null för att underlätta exemplet. Koden i det första tryblocket kontrollerar inte om det är null innan du försöker komma åt en Deque-metod. Koden i det andra tryblocket kontrollerar om det är null och initierar en implementering av Deque(LinkedList) om den är null. Resultatet från båda exemplen ser ut så här:

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) 

Meddelandet efter FEL i utgången ovan indikerar att a NullPointerExceptionkastas när ett metodanrop försöks på null Deque. Meddelandet efter INFO i utgången ovan indikerar att Dequeundantaget helt undviks genom att kontrollera om det är null först och sedan starta en ny implementering för det när det är null.

Detta tillvägagångssätt används ofta och kan, som visas ovan, vara mycket användbart för att undvika oönskade (oväntade) NullPointerExceptioninstanser. Det är dock inte utan kostnader. Att kontrollera null innan varje objekt används kan uppblåsa koden, kan vara tråkigt att skriva och öppnar mer utrymme för problem med utveckling och underhåll av tilläggskoden. Av denna anledning har det varit tal om att införa Java-språkstöd för inbyggd nolldetektering, automatisk tillsättning av dessa kontroller för null efter den initiala kodningen, nollsäkra typer, användning av Aspect-Oriented Programming (AOP) för att lägga till nollkontroll till byte-kod och andra nolldetekteringsverktyg.

Groovy tillhandahåller redan en bekväm mekanism för att hantera objektreferenser som är potentiellt null. Groovys operatör för säker navigering ( ?.) returnerar null snarare än att kasta a NullPointerExceptionnär en null-objektreferens nås.

Eftersom kontroll av null för varje objektreferens kan vara tråkig och uppblåser koden, väljer många utvecklare att på ett klokt sätt välja vilka objekt som ska kontrolleras för null. Detta leder vanligtvis till kontroll av noll på alla objekt av potentiellt okänt ursprung. Tanken här är att objekt kan kontrolleras vid exponerade gränssnitt och sedan antas vara säkra efter den första kontrollen.

Detta är en situation där den ternära operatören kan vara särskilt användbar. Istället för

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

den ternära operatören stöder denna mer kortfattade syntax

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

Kontrollera metodargument för Null

Den nyss diskuterade tekniken kan användas på alla objekt. Som anges i den tekniska beskrivningen väljer många utvecklare att bara kontrollera objekt för noll när de kommer från "opålitliga" källor. Detta innebär ofta att man testar för null först i metoder som utsätts för externa uppringare. I en viss klass kan till exempel utvecklaren välja att kontrollera om null på alla objekt som skickas till publicmetoder, men inte kontrollera om det finns null i privatemetoder.

Följande kod visar denna kontroll för null vid metodinmatning. Den innehåller en enda metod som den demonstrativa metoden som vänder om och kallar två metoder och skickar varje metod till ett enda nullargument. En av metoderna som tar emot ett nullargument kontrollerar argumentet för null först, men den andra antar bara att den skickade parametern inte är 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); } } 

När koden ovan körs visas utmatningen som visas nedan.

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. 

I båda fallen loggades ett felmeddelande. Fallet där en noll kontrollerades för kastade emellertid en annonserad IllegalArgumentException som innehöll ytterligare kontextinformation om när noll påträffades. Alternativt kunde denna null-parameter ha hanterats på olika sätt. För det fall där en null-parameter inte hanterades fanns det inga alternativ för hur man skulle hantera den. Många föredrar att kasta a NullPolinterExceptionmed ytterligare kontextinformation när en null uttryckligen upptäcks (se artikel 60 i andra upplagan av effektiv Java eller artikel 42 i första upplagan), men jag har en liten preferens för IllegalArgumentExceptionnär det uttryckligen är en metodargument som är null eftersom jag tror att mycket undantag lägger till kontextdetaljer och det är lätt att inkludera "null" i ämnet.

Tekniken för att kontrollera metodargument för null är verkligen en delmängd av den mer allmänna tekniken för att kontrollera alla objekt för null. Som beskrivits ovan är emellertid argument till offentligt exponerade metoder ofta minst betrodda i en applikation, så det kan vara viktigare att kontrollera dem än att kontrollera det genomsnittliga objektet för null.

Kontroll av metodparametrar för noll är också en delmängd av den mer allmänna praxis för att kontrollera metodparametrar för allmän giltighet som diskuteras i artikel 38 i andra upplagan av effektiv Java (artikel 23 i första upplagan).

Tänk på Primitives Snarare än Objekt

Jag tycker inte att det är en bra idé att välja en primitiv datatyp (t.ex. int) över dess motsvarande objektreferens (som heltal) helt enkelt för att undvika möjligheten till a NullPointerException, men det kan inte förnekas att en av fördelarna med primitiva typer är att de inte leder till NullPointerExceptions. Dock måste primitiva fortfarande kontrolleras för giltighet (en månad kan inte vara ett negativt heltal) och så kan denna fördel vara liten. Å andra sidan kan primitiva inte användas i Java-samlingar och det finns tillfällen man vill ha möjlighet att ställa in ett värde till null.

Det viktigaste är att vara mycket försiktig med kombinationen av primitiva, referenstyper och autoboxing. Det finns en varning i Effektiv Java (andra upplagan, artikel 49) angående farorna, inklusive kastning NullPointerException, relaterade till slarvig blandning av primitiva och referenstyper.

Överväg noggrant kedjade metodsamtal

A NullPointerExceptionkan vara väldigt lätt att hitta eftersom ett radnummer anger var det inträffade. En stapelspårning kan till exempel se ut som den som visas nedan:

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

Stapelspåret gör det uppenbart att det NullPointerExceptionkastades som ett resultat av kod som kördes på rad 222 av AvoidingNullPointerExamples.java. Även med det angivna linjenumret kan det fortfarande vara svårt att begränsa vilket objekt som är null om det finns flera objekt med metoder eller fält som kan nås på samma rad.

Till exempel har ett uttalande som someObject.getObjectA().getObjectB().getObjectC().toString();fyra möjliga samtal som kan ha kastat det som NullPointerExceptiontillskrivs samma kodrad. Att använda en felsökare kan hjälpa till med detta, men det kan finnas situationer när det är att föredra att helt enkelt bryta upp ovanstående kod så att varje samtal utförs på en separat linje. Detta gör att linjenumret i en stackspårning enkelt kan ange vilket exakt samtal som var problemet. Dessutom underlättar det explicit kontroll av varje objekt för null. Men på nackdelen ökar koden genom att bryta upp koden (för vissa är det positivt!) Och kanske inte alltid är önskvärt, speciellt om man är säker på att ingen av metoderna i fråga någonsin kommer att vara noll.

Gör NullPointerExceptions mer informativ

I ovanstående rekommendation var varningen att noga överväga att använda metodkedjekedjning främst för att det gjorde att linjenumret i stapelspåret var NullPointerExceptionmindre användbart än det annars skulle kunna vara. Radnumret visas dock bara i en stackspårning när koden kompilerades med felsökningsflaggan aktiverad. Om den kompilerades utan felsökning ser stapelspåret ut som det som visas nästa:

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

Som ovanstående utdata visar finns det ett metodnamn, men inte något radnummer för NullPointerException. Detta gör det svårare att omedelbart identifiera vad i koden som ledde till undantaget. Ett sätt att ta itu med detta är att tillhandahålla kontextinformation i alla kastade NullPointerException. Denna idé demonstrerades tidigare när en NullPointerExceptionfångades och kastades på nytt med ytterligare kontextinformation som en IllegalArgumentException. Men även om undantaget helt enkelt kastas om som ett annat NullPointerExceptionmed kontextinformation, är det fortfarande användbart. Kontextinformationen hjälper personen att felsöka koden för att snabbare identifiera den verkliga orsaken till problemet.

Följande exempel visar denna princip.

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

Resultatet från att köra ovanstående kod ser ut som följer.

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