Effektive Behandlung von Java NullPointerException

Es erfordert nicht viel Erfahrung in der Java-Entwicklung, um aus erster Hand zu erfahren, worum es bei der NullPointerException geht. Tatsächlich hat eine Person den Umgang damit als den Fehler Nummer eins hervorgehoben, den Java-Entwickler machen. Ich habe zuvor über die Verwendung von String.value (Object) gebloggt, um unerwünschte NullPointerExceptions zu reduzieren. Es gibt mehrere andere einfache Techniken, mit denen Sie das Auftreten dieser häufigen Art von RuntimeException, die seit JDK 1.0 bei uns ist, reduzieren oder eliminieren können. Dieser Blog-Beitrag sammelt und fasst einige der beliebtesten dieser Techniken zusammen.

Überprüfen Sie jedes Objekt vor der Verwendung auf Null

Der sicherste Weg, eine NullPointerException zu vermeiden, besteht darin, alle Objektreferenzen zu überprüfen, um sicherzustellen, dass sie nicht null sind, bevor Sie auf eines der Felder oder Methoden des Objekts zugreifen. Wie das folgende Beispiel zeigt, ist dies eine sehr einfache Technik.

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

Im obigen Code wird die verwendete Deque absichtlich auf Null initialisiert, um das Beispiel zu vereinfachen. Der Code im ersten tryBlock prüft nicht auf Null, bevor versucht wird, auf eine Deque-Methode zuzugreifen. Der Code im zweiten tryBlock prüft auf Null und instanziiert eine Implementierung der Deque(LinkedList), wenn sie Null ist. Die Ausgabe beider Beispiele sieht folgendermaßen aus:

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) 

Die Meldung nach ERROR in der obigen Ausgabe zeigt an, dass a NullPointerExceptionausgelöst wird, wenn ein Methodenaufruf für die Null versucht wird Deque. Die Meldung nach INFO in der obigen Ausgabe zeigt an, dass Dequedie Ausnahme insgesamt vermieden wurde , indem zuerst nach Null gesucht und dann eine neue Implementierung für sie instanziiert wurde, wenn sie null ist.

Dieser Ansatz wird häufig verwendet und kann, wie oben gezeigt, sehr nützlich sein, um unerwünschte (unerwartete) NullPointerExceptionInstanzen zu vermeiden . Es ist jedoch nicht ohne Kosten. Das Überprüfen auf Null vor der Verwendung jedes Objekts kann den Code aufblähen, das Schreiben langwierig machen und mehr Raum für Probleme bei der Entwicklung und Wartung des zusätzlichen Codes eröffnen. Aus diesem Grund wurde über die Einführung der Java-Sprachunterstützung für die integrierte Nullerkennung gesprochen, das automatische Hinzufügen dieser Überprüfungen auf Null nach der anfänglichen Codierung, nullsichere Typen und die Verwendung der aspektorientierten Programmierung (AOP) zum Hinzufügen der Nullprüfung Byte-Code und andere Null-Erkennungs-Tools.

Groovy bietet bereits einen praktischen Mechanismus für den Umgang mit Objektreferenzen, die möglicherweise null sind. Der sichere Navigationsoperator ( ?.) von Groovy gibt null zurück, anstatt a auszulösen, NullPointerExceptionwenn auf eine Null-Objektreferenz zugegriffen wird.

Da das Überprüfen von Null für jede Objektreferenz mühsam sein kann und den Code aufbläht, wählen viele Entwickler mit Bedacht aus, welche Objekte auf Null überprüft werden sollen. Dies führt normalerweise zur Überprüfung von Null bei allen Objekten potenziell unbekannter Herkunft. Die Idee dabei ist, dass Objekte an exponierten Schnittstellen überprüft und nach der ersten Überprüfung als sicher angenommen werden können.

Dies ist eine Situation, in der der ternäre Operator besonders nützlich sein kann. Anstatt von

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

Der ternäre Operator unterstützt diese präzisere Syntax

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

Überprüfen Sie die Methodenargumente auf Null

Die soeben diskutierte Technik kann auf alle Objekte angewendet werden. Wie in der Beschreibung dieser Technik angegeben, prüfen viele Entwickler Objekte nur dann auf Null, wenn sie aus "nicht vertrauenswürdigen" Quellen stammen. Dies bedeutet häufig, dass Methoden, die externen Anrufern ausgesetzt sind, als Erstes auf Null getestet werden. In einer bestimmten Klasse kann der Entwickler beispielsweise festlegen, dass alle an publicMethoden übergebenen Objekte auf Null überprüft werden, in privateMethoden jedoch nicht auf Null .

Der folgende Code demonstriert diese Überprüfung auf Null bei der Methodeneingabe. Es enthält eine einzelne Methode als demonstrative Methode, die sich umdreht und zwei Methoden aufruft, wobei jeder Methode ein einzelnes Nullargument übergeben wird. Eine der Methoden, die ein Null-Argument erhalten, überprüft dieses Argument zuerst auf Null, die andere geht jedoch nur davon aus, dass der übergebene Parameter nicht Null ist.

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

Wenn der obige Code ausgeführt wird, wird die Ausgabe wie folgt angezeigt.

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 beiden Fällen wurde eine Fehlermeldung protokolliert. Der Fall, in dem eine Null überprüft wurde, löste jedoch eine angekündigte IllegalArgumentException aus, die zusätzliche Kontextinformationen darüber enthielt, wann die Null gefunden wurde. Alternativ könnte dieser Nullparameter auf verschiedene Arten behandelt worden sein. Für den Fall, dass ein Nullparameter nicht behandelt wurde, gab es keine Optionen für die Behandlung. Viele Leute ziehen es vor, ein NullPolinterExceptionmit den zusätzlichen Kontextinformationen zu werfen, wenn eine Null explizit entdeckt wird (siehe Punkt 60 in der zweiten Ausgabe von Effective Java oder Punkt 42 in der ersten Ausgabe), aber ich habe eine leichte Präferenz dafür, IllegalArgumentExceptionwenn es explizit eine ist Methodenargument, das null ist, weil ich denke, dass die Ausnahme Kontextdetails hinzufügt und es einfach ist, "null" in den Betreff aufzunehmen.

Die Technik zum Überprüfen von Methodenargumenten auf Null ist wirklich eine Teilmenge der allgemeineren Technik zum Überprüfen aller Objekte auf Null. Wie oben dargelegt, sind Argumente für öffentlich zugängliche Methoden in einer Anwendung jedoch häufig am wenigsten vertrauenswürdig. Daher ist es möglicherweise wichtiger, sie zu überprüfen, als das durchschnittliche Objekt auf Null zu überprüfen.

Das Überprüfen von Methodenparametern auf Null ist auch eine Teilmenge der allgemeineren Praxis des Überprüfens von Methodenparametern auf allgemeine Gültigkeit, wie in Punkt 38 ​​der zweiten Ausgabe von Effective Java (Punkt 23 in der ersten Ausgabe) erläutert .

Betrachten Sie eher Primitive als Objekte

Ich denke nicht, dass es eine gute Idee ist, einen primitiven Datentyp (wie z. B. int) gegenüber seinem entsprechenden Objektreferenztyp (wie z. B. Integer) auszuwählen , um die Möglichkeit eines zu vermeiden NullPointerException, aber es ist nicht zu leugnen, dass einer der Vorteile von primitive Typen sind, dass sie nicht zu NullPointerExceptions führen. Grundelemente müssen jedoch noch auf ihre Gültigkeit überprüft werden (ein Monat kann keine negative Ganzzahl sein), sodass dieser Vorteil möglicherweise gering ist. Andererseits können Grundelemente in Java-Sammlungen nicht verwendet werden, und manchmal möchte man die Möglichkeit haben, einen Wert auf Null zu setzen.

Das Wichtigste ist, bei der Kombination von Grundelementen, Referenztypen und Autoboxen sehr vorsichtig zu sein. In Effective Java (Zweite Ausgabe, Punkt 49) wird eine Warnung bezüglich der Gefahren, einschließlich des Werfens NullPointerException, im Zusammenhang mit dem unachtsamen Mischen von primitiven und Referenztypen angezeigt.

Betrachten Sie verkettete Methodenaufrufe sorgfältig

A NullPointerExceptionkann sehr leicht zu finden sein, da eine Zeilennummer angibt, wo es aufgetreten ist. Beispielsweise könnte eine Stapelverfolgung wie folgt aussehen:

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

Die Stapelverfolgung macht deutlich, dass der NullPointerExceptionals Ergebnis des in Zeile 222 von ausgeführten Codes ausgelöst wurde AvoidingNullPointerExamples.java. Selbst mit der angegebenen Zeilennummer kann es schwierig sein, einzugrenzen, welches Objekt null ist, wenn mehrere Objekte mit Methoden oder Feldern in derselben Zeile aufgerufen werden.

Zum Beispiel hat eine Anweisung wie someObject.getObjectA().getObjectB().getObjectC().toString();vier mögliche Aufrufe, die das NullPointerExceptionAttribut auf dieselbe Codezeile geworfen haben könnten . Die Verwendung eines Debuggers kann dabei helfen, aber es kann Situationen geben, in denen es vorzuziehen ist, den obigen Code einfach aufzubrechen, damit jeder Aufruf in einer separaten Zeile ausgeführt wird. Auf diese Weise kann die in einer Stapelverfolgung enthaltene Zeilennummer leicht angeben, welcher genaue Anruf das Problem war. Darüber hinaus erleichtert es die explizite Überprüfung jedes Objekts auf Null. Auf der anderen Seite erhöht das Aufbrechen des Codes die Anzahl der Codezeilen (auf einige, die positiv sind!) Und ist möglicherweise nicht immer wünschenswert, insbesondere wenn man sicher ist, dass keine der fraglichen Methoden jemals null sein wird.

Machen Sie NullPointerExceptions informativer

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) 

Wie die obige Ausgabe zeigt, gibt es einen Methodennamen, aber keine Zeilennummer für die NullPointerException. Dies macht es schwieriger, sofort zu identifizieren, was im Code zur Ausnahme geführt hat. Eine Möglichkeit, dies zu beheben, besteht darin, Kontextinformationen in jedem ausgelösten Objekt bereitzustellen NullPointerException. Diese Idee wurde früher demonstriert, als a NullPointerExceptiongefangen und mit zusätzlichen Kontextinformationen als a erneut geworfen wurde IllegalArgumentException. Selbst wenn die Ausnahme einfach als eine andere NullPointerExceptionmit Kontextinformationen erneut ausgelöst wird , ist sie dennoch hilfreich. Die Kontextinformationen helfen der Person, die den Code debuggt, die wahre Ursache des Problems schneller zu identifizieren.

Das folgende Beispiel zeigt dieses Prinzip.

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

Die Ausgabe beim Ausführen des obigen Codes sieht wie folgt aus.

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