Gestione efficace di Java NullPointerException

Non ci vuole molta esperienza di sviluppo Java per imparare in prima persona di cosa tratta la NullPointerException. In effetti, una persona ha evidenziato che si tratta dell'errore numero uno degli sviluppatori Java. Ho scritto in precedenza sul blog sull'uso di String.value (Object) per ridurre NullPointerExceptions indesiderate. Esistono molte altre semplici tecniche che è possibile utilizzare per ridurre o eliminare le occorrenze di questo tipo comune di RuntimeException che è stato con noi da JDK 1.0. Questo post del blog raccoglie e riassume alcune delle più popolari di queste tecniche.

Controllare ogni oggetto per nullo prima dell'uso

Il modo più sicuro per evitare una NullPointerException è controllare tutti i riferimenti agli oggetti per assicurarsi che non siano nulli prima di accedere a uno dei campi o dei metodi dell'oggetto. Come indica il seguente esempio, questa è una tecnica molto semplice.

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

Nel codice precedente, il Deque utilizzato è intenzionalmente inizializzato a null per facilitare l'esempio. Il codice nel primo tryblocco non verifica la presenza di null prima di tentare di accedere a un metodo Deque. Il codice nel secondo tryblocco verifica la presenza di null e crea un'istanza di un'implementazione di Deque(LinkedList) se è null. L'output di entrambi gli esempi è simile a questo:

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) 

Il messaggio che segue ERROR nell'output precedente indica che NullPointerExceptionviene generato un quando viene tentata una chiamata al metodo su null Deque. Il messaggio che segue INFO nell'output precedente indica che verificando Dequeprima la presenza di null e quindi istanziando una nuova implementazione quando è null, l'eccezione è stata evitata del tutto.

Questo approccio viene spesso utilizzato e, come mostrato sopra, può essere molto utile per evitare NullPointerExceptionistanze indesiderate (impreviste) . Tuttavia, non è privo di costi. Verificare la presenza di null prima di utilizzare ogni oggetto può gonfiare il codice, può essere noioso da scrivere e apre più spazio a problemi con lo sviluppo e la manutenzione del codice aggiuntivo. Per questo motivo, si è parlato di introdurre il supporto del linguaggio Java per il rilevamento di null integrato, l'aggiunta automatica di questi controlli per null dopo la codifica iniziale, tipi null-safe, uso di Aspect-Oriented Programming (AOP) per aggiungere il controllo di null in byte code e altri strumenti di rilevamento null.

Groovy fornisce già un comodo meccanismo per gestire i riferimenti a oggetti potenzialmente nulli. L'operatore di navigazione sicura di Groovy ( ?.) restituisce null anziché lanciare un NullPointerExceptionquando si accede a un riferimento a un oggetto null.

Poiché il controllo di null per ogni riferimento a un oggetto può essere noioso e gonfiare il codice, molti sviluppatori scelgono di selezionare con giudizio gli oggetti da controllare per null. Questo in genere porta al controllo di null su tutti gli oggetti di origini potenzialmente sconosciute. L'idea qui è che gli oggetti possono essere controllati sulle interfacce esposte e quindi si presume che siano sicuri dopo il controllo iniziale.

Questa è una situazione in cui l'operatore ternario può essere particolarmente utile. Invece di

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

l'operatore ternario supporta questa sintassi più concisa

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

Controllare gli argomenti del metodo per null

La tecnica appena discussa può essere utilizzata su tutti gli oggetti. Come affermato nella descrizione di quella tecnica, molti sviluppatori scelgono di controllare solo gli oggetti per null quando provengono da fonti "non attendibili". Questo spesso significa testare per prima cosa nulla nei metodi esposti a chiamanti esterni. Ad esempio, in una particolare classe, lo sviluppatore potrebbe scegliere di verificare la presenza di null su tutti gli oggetti passati ai publicmetodi, ma non di verificare la presenza di null nei privatemetodi.

Il codice seguente dimostra questo controllo per null all'ingresso del metodo. Include un singolo metodo come metodo dimostrativo che gira intorno e chiama due metodi, passando a ciascun metodo un singolo argomento null. Uno dei metodi che ricevono un argomento nullo verifica prima che l'argomento sia nullo, ma l'altro presume che il parametro passato non sia nullo.

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

Quando viene eseguito il codice precedente, l'output viene visualizzato come mostrato di seguito.

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 entrambi i casi è stato registrato un messaggio di errore. Tuttavia, il caso in cui è stato verificato un valore null ha generato un'eccezione IllegalArgumentException pubblicizzata che includeva informazioni di contesto aggiuntive su quando è stato rilevato il valore null. In alternativa, questo parametro nullo avrebbe potuto essere gestito in diversi modi. Per il caso in cui un parametro null non è stato gestito, non c'erano opzioni su come gestirlo. Molte persone preferiscono lanciare un NullPolinterExceptioncon le informazioni di contesto aggiuntive quando viene scoperto esplicitamente un null (vedere l'articolo # 60 nella seconda edizione di Java effettivo o l'elemento # 42 nella prima edizione), ma ho una leggera preferenza per IllegalArgumentExceptionquando è esplicitamente un argomento del metodo che è nullo perché penso che la stessa eccezione aggiunga dettagli sul contesto ed è facile includere "null" nell'oggetto.

La tecnica di controllo degli argomenti del metodo per null è in realtà un sottoinsieme della tecnica più generale di controllo di tutti gli oggetti per null. Tuttavia, come indicato sopra, gli argomenti relativi ai metodi esposti pubblicamente sono spesso i meno affidabili in un'applicazione e quindi controllarli potrebbe essere più importante che verificare la presenza di null nell'oggetto medio.

Il controllo dei parametri del metodo per null è anche un sottoinsieme della pratica più generale di controllo dei parametri del metodo per la validità generale, come discusso nell'elemento # 38 della seconda edizione di Java effettivo (elemento 23 nella prima edizione).

Considera le primitive piuttosto che gli oggetti

Non penso sia una buona idea selezionare un tipo di dati primitivo (come int) sul suo corrispondente tipo di riferimento all'oggetto (come Integer) semplicemente per evitare la possibilità di un NullPointerException, ma non si può negare che uno dei vantaggi di tipi primitivi è che non portano a NullPointerExceptions. Tuttavia, le primitive devono ancora essere verificate per la validità (un mese non può essere un numero intero negativo) e quindi questo vantaggio potrebbe essere piccolo. D'altra parte, le primitive non possono essere utilizzate nelle collezioni Java e ci sono volte in cui si desidera la possibilità di impostare un valore su null.

La cosa più importante è essere molto cauti sulla combinazione di primitive, tipi di riferimento e autoboxing. C'è un avvertimento in Effective Java (Seconda Edizione, Articolo # 49) riguardante i pericoli, incluso il lancio di NullPointerException, relativi alla mescolanza imprudente di tipi primitivi e di riferimento.

Considera attentamente le chiamate di metodo concatenate

A NullPointerExceptionpuò essere molto facile da trovare perché un numero di riga indicherà dove si è verificato. Ad esempio, una traccia dello stack potrebbe essere simile a quella mostrata di seguito:

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

La traccia dello stack rende ovvio che è NullPointerExceptionstato lanciato come risultato del codice eseguito sulla riga 222 di AvoidingNullPointerExamples.java. Anche con il numero di riga fornito, può essere ancora difficile restringere quale oggetto è nullo se ci sono più oggetti con metodi o campi a cui si accede sulla stessa riga.

Ad esempio, un'istruzione come someObject.getObjectA().getObjectB().getObjectC().toString();ha quattro possibili chiamate che potrebbero aver generato l' NullPointerExceptionattributo alla stessa riga di codice. L'uso di un debugger può aiutare in questo, ma potrebbero esserci situazioni in cui è preferibile interrompere semplicemente il codice precedente in modo che ogni chiamata venga eseguita su una riga separata. Ciò consente al numero di riga contenuto in un'analisi dello stack di indicare facilmente quale chiamata esatta era il problema. Inoltre, facilita il controllo esplicito di ogni oggetto per null. Tuttavia, il rovescio della medaglia, la scomposizione del codice aumenta la riga di conteggio del codice (per alcuni è positivo!) E potrebbe non essere sempre desiderabile, soprattutto se si è certi che nessuno dei metodi in questione sarà mai nullo.

Rendi NullPointerExceptions più informativo

Nella raccomandazione di cui sopra, l'avvertimento era di considerare attentamente l'uso del concatenamento di chiamate di metodo principalmente perché rendeva il numero di riga nella traccia dello stack NullPointerExceptionmeno utile di quanto sarebbe altrimenti. Tuttavia, il numero di riga viene visualizzato solo in un'analisi dello stack quando il codice è stato compilato con il flag di debug attivato. Se è stato compilato senza debug, la traccia dello stack è simile a quella mostrata di seguito:

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

Come dimostra l'output precedente, esiste un nome di metodo, ma non un numero di riga per il file NullPointerException. Ciò rende più difficile identificare immediatamente cosa nel codice ha portato all'eccezione. Un modo per risolvere questo problema è fornire informazioni di contesto in ogni lancio NullPointerException. Questa idea è stata dimostrata in precedenza quando a è NullPointerExceptionstato catturato e rilanciato con informazioni di contesto aggiuntive come a IllegalArgumentException. Tuttavia, anche se l'eccezione viene semplicemente rilanciata come un'altra NullPointerExceptioncon informazioni sul contesto, è comunque utile. Le informazioni di contesto aiutano la persona a eseguire il debug del codice a identificare più rapidamente la vera causa del problema.

Il seguente esempio dimostra questo principio.

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

L'output dell'esecuzione del codice precedente è il seguente.

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