Effectieve verwerking van Java NullPointerException

Er is niet veel Java-ontwikkelervaring voor nodig om uit de eerste hand te leren waar de NullPointerException over gaat. In feite heeft één persoon benadrukt hoe hiermee om te gaan als de grootste fout die Java-ontwikkelaars maken. Ik blogde eerder over het gebruik van String.value (Object) om ongewenste NullPointerExceptions te verminderen. Er zijn verschillende andere eenvoudige technieken die u kunt gebruiken om het voorkomen van dit veel voorkomende type RuntimeException, dat bij ons is sinds JDK 1.0, te verminderen of te elimineren. Deze blogpost verzamelt en vat enkele van de meest populaire van deze technieken samen.

Controleer elk object op nul voordat u het gebruikt

De meest zekere manier om een ​​NullPointerException te vermijden, is door alle objectreferenties te controleren om er zeker van te zijn dat ze niet null zijn voordat u een van de velden of methoden van het object opent. Zoals het volgende voorbeeld aangeeft, is dit een heel eenvoudige techniek.

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

In de bovenstaande code is de gebruikte Deque opzettelijk op nul geïnitialiseerd om het voorbeeld te vergemakkelijken. De code in het eerste tryblok controleert niet op null voordat wordt geprobeerd toegang te krijgen tot een Deque-methode. De code in het tweede tryblok controleert op null en instantieert een implementatie van de Deque(LinkedList) als deze null is. De uitvoer van beide voorbeelden ziet er als volgt uit:

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) 

Het bericht na ERROR in de bovenstaande uitvoer geeft aan dat een NullPointerExceptionwordt gegenereerd wanneer een methodeaanroep wordt geprobeerd op de null Deque. Het bericht dat volgt op INFO in de bovenstaande uitvoer geeft aan dat door Dequeeerst op null te controleren en vervolgens een nieuwe implementatie ervoor te instantiëren wanneer deze null is, de uitzondering volledig werd vermeden.

Deze benadering wordt vaak gebruikt en kan, zoals hierboven getoond, zeer nuttig zijn om ongewenste (onverwachte) NullPointerExceptiongevallen te vermijden . Het is echter niet zonder kosten. Het controleren op null voordat elk object wordt gebruikt, kan de code opzwellen, kan vervelend zijn om te schrijven en opent meer ruimte voor problemen met de ontwikkeling en het onderhoud van de aanvullende code. Om deze reden is er gesproken over de introductie van Java-taalondersteuning voor ingebouwde null-detectie, het automatisch toevoegen van deze checks op null na de initiële codering, null-safe types, gebruik van Aspect-Oriented Programming (AOP) om null-controle toe te voegen naar byte code, en andere null-detectie tools.

Groovy biedt al een handig mechanisme voor het omgaan met objectreferenties die mogelijk nul zijn. De veilige navigatie-operator van Groovy ( ?.) retourneert null in plaats van a NullPointerExceptionwanneer een null-objectreferentie wordt benaderd.

Omdat het controleren van null voor elke objectreferentie vervelend kan zijn en de code opzwelt, kiezen veel ontwikkelaars ervoor om oordeelkundig te selecteren welke objecten ze op null moeten controleren. Dit leidt doorgaans tot het controleren van nul op alle objecten met een mogelijk onbekende oorsprong. Het idee hier is dat objecten kunnen worden gecontroleerd op blootgestelde interfaces en vervolgens kunnen worden aangenomen dat ze veilig zijn na de eerste controle.

Dit is een situatie waarin de ternaire operator bijzonder nuttig kan zijn. In plaats van

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

de ternaire operator ondersteunt deze meer beknopte syntaxis

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

Controleer Method Arguments voor Null

De zojuist besproken techniek kan op alle objecten worden gebruikt. Zoals vermeld in de beschrijving van die techniek, kiezen veel ontwikkelaars ervoor om objecten alleen op null te controleren als ze afkomstig zijn van "niet-vertrouwde" bronnen. Dit betekent vaak als eerste testen op null in methoden die worden blootgesteld aan externe bellers. In een bepaalde klasse kan de ontwikkelaar er bijvoorbeeld voor kiezen om te controleren op null voor alle objecten die aan publicmethoden zijn doorgegeven , maar niet om te controleren op null in privatemethoden.

De volgende code demonstreert deze controle op null bij het invoeren van een methode. Het bevat een enkele methode als de demonstratieve methode die zich omdraait en twee methoden aanroept, waarbij elke methode een enkel nul-argument doorgeeft. Een van de methoden die een null-argument ontvangt, controleert dat argument eerst op null, maar de andere neemt alleen aan dat de doorgegeven parameter niet null is.

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

Wanneer de bovenstaande code wordt uitgevoerd, verschijnt de uitvoer zoals hieronder wordt weergegeven.

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. 

In beide gevallen werd een foutmelding gelogd. Het geval waarin een null werd gecontroleerd, gooide echter een geadverteerde IllegalArgumentException die aanvullende contextinformatie bevatte over wanneer de null werd aangetroffen. Als alternatief had deze null-parameter op verschillende manieren kunnen worden afgehandeld. Voor het geval waarin een null-parameter niet werd afgehandeld, waren er geen opties om deze af te handelen. Veel mensen geven er de voorkeur aan om a te gooien NullPolinterExceptionmet de aanvullende contextinformatie wanneer een null expliciet wordt ontdekt (zie Item # 60 in de tweede editie van Effective Java of Item # 42 in de eerste editie), maar ik heb een lichte voorkeur voor IllegalArgumentExceptionwanneer het expliciet een method-argument dat null is omdat ik denk dat de uitzondering zelf contextdetails toevoegt en het gemakkelijk is om "null" in het onderwerp op te nemen.

De techniek om methodeargumenten op null te controleren is in feite een subset van de meer algemene techniek om alle objecten op null te controleren. Zoals hierboven uiteengezet, worden argumenten voor openbaar blootgestelde methoden echter vaak het minst vertrouwd in een toepassing en daarom kan het controleren ervan belangrijker zijn dan het controleren van het gemiddelde object op null.

Het controleren van methodeparameters op null is ook een subset van de meer algemene praktijk van het controleren van methodeparameters op algemene geldigheid, zoals besproken in Item # 38 van de tweede editie van Effective Java (Item 23 in First Edition).

Overweeg primitieven in plaats van objecten

Ik denk niet dat het een goed idee is om een ​​primitief gegevenstype (zoals int) te selecteren boven het corresponderende objectreferentietype (zoals geheel getal) om de mogelijkheid van een te vermijden NullPointerException, maar het valt niet te ontkennen dat een van de voordelen van primitieve typen is dat ze niet leiden tot NullPointerExceptions. Primitieven moeten echter nog steeds op geldigheid worden gecontroleerd (een maand mag geen negatief geheel getal zijn) en daarom kan dit voordeel klein zijn. Aan de andere kant kunnen primitieven niet worden gebruikt in Java Collections en soms wil men de mogelijkheid hebben om een ​​waarde op null in te stellen.

Het belangrijkste is om erg voorzichtig te zijn met de combinatie van primitieven, referentietypes en autoboxing. Er is een waarschuwing in Effective Java (Second Edition, Item # 49) met betrekking tot de gevaren, inclusief het gooien van NullPointerException, gerelateerd aan het onzorgvuldig mengen van primitieve en referentietypes.

Overweeg zorgvuldig geketende methodeaanroepen

Een NullPointerExceptionkan heel gemakkelijk te vinden zijn omdat een regelnummer aangeeft waar het is opgetreden. Een stacktracering kan er bijvoorbeeld zo uitzien als hieronder wordt weergegeven:

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

De stacktracering maakt het duidelijk dat de NullPointerExceptionis gegooid als resultaat van code die is uitgevoerd op regel 222 van AvoidingNullPointerExamples.java. Zelfs met het opgegeven regelnummer kan het nog steeds moeilijk zijn om te bepalen welk object nul is als er meerdere objecten zijn met methoden of velden die op dezelfde regel worden benaderd.

Een instructie zoals someObject.getObjectA().getObjectB().getObjectC().toString();heeft bijvoorbeeld vier mogelijke aanroepen die mogelijk de NullPointerExceptiontoegeschreven aan dezelfde regel code hebben geworpen . Het gebruik van een debugger kan hierbij helpen, maar er kunnen situaties zijn waarin het de voorkeur verdient om de bovenstaande code eenvoudigweg op te splitsen, zodat elke oproep op een aparte regel wordt uitgevoerd. Hierdoor kan het regelnummer in een stacktracering gemakkelijk aangeven welke exacte oproep het probleem was. Bovendien vergemakkelijkt het het expliciet controleren van elk object op null. Het nadeel is echter dat het opbreken van de code het aantal codes verhoogt (voor sommigen is dat positief!) En is misschien niet altijd wenselijk, vooral als je zeker weet dat geen van de methoden in kwestie ooit nul zal zijn.

Maak NullPointerExceptions informatiever

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) 

Zoals de bovenstaande uitvoer laat zien, is er een methode-naam, maar geen regelnummer voor de NullPointerException. Dit maakt het moeilijker om direct te identificeren wat in de code tot de uitzondering heeft geleid. Een manier om dit aan te pakken, is door contextinformatie te verstrekken in elke throw NullPointerException. Dit idee werd eerder gedemonstreerd toen a NullPointerExceptionwerd gevangen en opnieuw werd gegooid met aanvullende contextinformatie als een IllegalArgumentException. Maar zelfs als de uitzondering gewoon opnieuw wordt gegooid als een andere NullPointerExceptionmet contextinformatie, is het nog steeds nuttig. De contextinformatie helpt de persoon die de code debugt om sneller de ware oorzaak van het probleem te identificeren.

Het volgende voorbeeld illustreert dit principe.

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

De uitvoer van het uitvoeren van de bovenstaande code ziet er als volgt uit.

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