Java Language
Java-Fallstricke - Nulls und NullPointerException
Suche…
Bemerkungen
Der Wert null
ist der Standardwert für einen nicht initialisierten Wert eines Felds, dessen Typ ein Referenztyp ist.
NullPointerException
(oder NPE) ist die Ausnahme, die ausgelöst wird, wenn Sie versuchen, eine unangemessene Operation für den null
Objektverweis auszuführen. Solche Operationen umfassen:
- Aufruf einer Instanzmethode für ein
null
- Zugriff auf ein Feld eines
null
, - Versuch, ein
null
Array-Objekt zu indizieren oder auf seine Länge zuzugreifen - Verwenden einer
null
als Mutex in einemsynchronized
Block, - Casting einer
null
Objektreferenz - Unboxing einer
null
und - eine
null
Objektreferenz werfen.
Die häufigsten Ursachen für NPEs:
- Vergessen, ein Feld mit einem Referenztyp zu initialisieren,
- Vergessen, Elemente eines Arrays eines Referenztyps zu initialisieren, oder
- nicht die Prüfung der Ergebnisse bestimmter API - Methoden , die als wiederkehrende spezifiziert sind
null
unter bestimmten Umständen.
Beispiele für häufig verwendete Methoden, die null
sind:
- Die Methode
get(key)
in derMap
API gibt einenull
wenn Sie sie mit einem Schlüssel aufrufen, der keine Zuordnung hat. - Die
getResource(path)
undgetResourceAsStream(path)
in denClassLoader
undClass
APIs gebennull
wenn die Ressource nicht gefunden werden kann. - Die
get()
-Methode in derReference
API gibtnull
wenn der Garbage Collector die Referenz gelöscht hat. - Verschiedene
getXxxx
Methoden in den Java EE-Servlet-APIs gebennull
wenn Sie versuchen, einen nicht vorhandenen Anforderungsparameter, eine Sitzung oder ein Sitzungsattribut usw. abzurufen.
Es gibt Strategien, um unerwünschte NPEs zu vermeiden, z. B. das explizite Testen auf null
oder die Verwendung von "Yoda Notation". Diese Strategien haben jedoch oft das unerwünschte Ergebnis, dass Probleme in Ihrem Code verborgen werden, die wirklich behoben werden müssen.
Fallstricke - Unnötige Verwendung von primitiven Wrappern kann zu NullPointerExceptions führen
Manchmal verwenden Programmierer mit neuem Java primitive Typen und Wrapper austauschbar. Dies kann zu Problemen führen. Betrachten Sie dieses Beispiel:
public class MyRecord {
public int a, b;
public Integer c, d;
}
...
MyRecord record = new MyRecord();
record.a = 1; // OK
record.b = record.b + 1; // OK
record.c = 1; // OK
record.d = record.d + 1; // throws a NullPointerException
Unsere MyRecord
Klasse 1 MyRecord
auf der Standardinitialisierung, um die Werte in ihren Feldern zu initialisieren. Wenn wir also new
eine Aufzeichnung, die a
und b
werden Felder auf Null gesetzt werden, und die c
und d
Felder gesetzt werden null
.
Wenn wir versuchen, die standardmäßig initialisierten Felder zu verwenden, sehen wir, dass die int
Felder die ganze Zeit funktionieren, aber die Integer
Felder funktionieren in einigen Fällen und nicht in anderen. In dem Fall, dass ein null
NullPointerException
(mit d
), tritt der Ausdruck rechts auf der rechten Seite auf, einen null
Verweis NullPointerException
, was dazu führt, dass die NullPointerException
ausgelöst wird.
Es gibt mehrere Möglichkeiten, dies zu betrachten:
Wenn die Felder
c
undd
primitive Wrapper sein müssen, sollten wir uns entweder nicht auf die Standardinitialisierung verlassen oder aufnull
testen. Für ersteres gilt der richtige Ansatz, es sei denn, es gibt eine eindeutige Bedeutung für die Felder imnull
.Wenn es sich bei den Feldern nicht um primitive Wrapper handeln muss, ist es ein Fehler, sie als primitive Wrapper festzulegen. Zusätzlich zu diesem Problem haben die primitiven Wrapper relativ zu primitiven Typen zusätzlichen Aufwand.
Die Lektion hier ist, keine primitiven Wrapper-Typen zu verwenden, es sei denn, Sie müssen es wirklich tun.
1 - Diese Klasse ist kein Beispiel für eine gute Kodierungspraxis. Zum Beispiel hätte eine gut entworfene Klasse keine öffentlichen Felder. Dies ist jedoch nicht der Sinn dieses Beispiels.
Pitfall - Verwenden von null zur Darstellung eines leeren Arrays oder einer leeren Sammlung
Einige Programmierer denken, dass es eine gute Idee ist, Platz zu sparen, indem eine null
, um ein leeres Array oder eine leere Sammlung darzustellen. Es ist zwar richtig, dass Sie wenig Speicherplatz sparen können, der Code wird jedoch auf der anderen Seite komplizierter und anfälliger. Vergleichen Sie diese beiden Versionen einer Methode zum Summieren eines Arrays:
In der ersten Version wird normalerweise die Methode codiert:
/**
* Sum the values in an array of integers.
* @arg values the array to be summed
* @return the sum
**/
public int sum(int[] values) {
int sum = 0;
for (int value : values) {
sum += value;
}
return sum;
}
Die zweite Version ist, wie Sie die Methode codieren müssen, wenn Sie normalerweise null
, um ein leeres Array darzustellen.
/**
* Sum the values in an array of integers.
* @arg values the array to be summed, or null.
* @return the sum, or zero if the array is null.
**/
public int sum(int[] values) {
int sum = 0;
if (values != null) {
for (int value : values) {
sum += value;
}
}
return sum;
}
Wie Sie sehen, ist der Code etwas komplizierter. Dies ist direkt auf die Entscheidung zurückzuführen, null
auf diese Weise zu verwenden.
Überlegen Sie nun, ob dieses Array, das möglicherweise eine null
ist, an vielen Stellen verwendet wird. An jedem Ort, an dem Sie es verwenden, müssen Sie überlegen, ob Sie auf null
testen müssen. Wenn Sie eine verpassen null
Test, der es sein muss, riskieren Sie eine NullPointerException
. Daher führt die Strategie der Verwendung von null
auf diese Weise dazu, dass Ihre Anwendung fragiler wird. dh anfälliger für die Folgen von Programmierfehlern.
Die Lektion hier ist, leere Arrays und leere Listen zu verwenden, wenn Sie das meinen.
int[] values = new int[0]; // always empty
List<Integer> list = new ArrayList(); // initially empty
List<Integer> list = Collections.emptyList(); // always empty
Der Platzaufwand ist gering, und es gibt andere Möglichkeiten, ihn zu minimieren, wenn sich dies lohnt.
Pitfall - unerwartete Nullen "gut machen"
Auf StackOverflow wird in den Antworten häufig Code wie folgt angezeigt:
public String joinStrings(String a, String b) {
if (a == null) {
a = "";
}
if (b == null) {
b = "";
}
return a + ": " + b;
}
Dies wird häufig von einer Behauptung begleitet, die "bewährte NullPointerException
" ist, um auf null
zu testen, um NullPointerException
zu vermeiden.
Ist es die beste Praxis? Kurz gesagt: Nein.
Es gibt einige Grundannahmen, die hinterfragt werden müssen, bevor wir sagen können, ob dies in unseren joinStrings
:
Was bedeutet es, dass "a" oder "b" null ist?
Ein String
Wert kann null oder mehr Zeichen sein, daher haben wir bereits die Möglichkeit, einen leeren String darzustellen. Bedeutet null
etwas anderes als ""
? Wenn nein, ist es problematisch, eine leere Zeichenfolge auf zwei Arten darzustellen.
Kommt die Null aus einer nicht initialisierten Variablen?
Eine null
kann aus einem nicht initialisierten Feld oder einem nicht initialisierten Array-Element stammen. Der Wert kann von Entwurf oder von Zufall nicht initialisiert werden. Wenn es ein Zufall war, dann ist dies ein Fehler.
Steht die Null für "Weiß nicht" oder "Fehlender Wert"?
Manchmal kann eine null
eine echte Bedeutung haben. B., dass der tatsächliche Wert einer Variablen unbekannt oder nicht verfügbar oder "optional" ist. In Java 8 bietet die Optional
Klasse eine bessere Möglichkeit, dies auszudrücken.
Wenn dies ein Fehler (oder ein Konstruktionsfehler) ist, sollten wir "gut machen"?
Eine Interpretation des Codes besteht darin, dass wir eine unerwartete null
durch eine leere Zeichenfolge ersetzen. Ist die richtige Strategie? Wäre es besser, die NullPointerException
passieren zu lassen, die Ausnahme weiter oben im Stack NullPointerException
und als Fehler zu protokollieren?
Das Problem beim "Wiedergutmachen" besteht darin, dass das Problem entweder verdeckt wird oder die Diagnose schwieriger wird.
Ist das effizient / gut für die Codequalität?
Wenn der "Make Good" -Ansatz konsequent verwendet wird, enthält Ihr Code viele "defensive" Nulltests. Dies macht es länger und schwieriger zu lesen. Außerdem können all diese Tests und "Wiedergutmachung" die Leistung Ihrer Anwendung beeinträchtigen.
in Summe
Wenn null
ein sinnvoller Wert ist, ist der korrekte Ansatz der Test auf den null
. Die Folge ist, dass, wenn ein null
sinnvoll ist, dieser in den Javadocs von Methoden, die den null
akzeptieren oder ihn zurückgeben, eindeutig dokumentiert werden sollte.
Andernfalls ist es eine bessere Idee, eine unerwartete null
als Programmierfehler zu behandeln und die NullPointerException
passieren zu lassen, damit der Entwickler NullPointerException
, dass ein Problem im Code vorliegt.
Pitfall - Rückgabe von Null anstelle einer Ausnahme
Einige Java-Programmierer haben eine generelle Abneigung gegen das Auslösen oder Weiterleiten von Ausnahmen. Dies führt zu Code wie dem folgenden:
public Reader getReader(String pathname) {
try {
return new BufferedReader(FileReader(pathname));
} catch (IOException ex) {
System.out.println("Open failed: " + ex.getMessage());
return null;
}
}
Was ist das Problem damit?
Das Problem ist, dass der getReader
eine null
als speziellen Wert getReader
um anzuzeigen, dass der Reader
nicht geöffnet werden konnte. Nun muss der Rückgabewert überprüft werden , um zu sehen , ob es null
, bevor sie verwendet wird. Wenn der Test ausgelassen wird, ist das Ergebnis eine NullPointerException
.
Hier gibt es eigentlich drei Probleme:
- Die
IOException
wurde zu früh aufgefangen. - Aufgrund des Aufbaus dieses Codes besteht die Gefahr, dass eine Ressource ausläuft.
- Eine
null
wurde verwendet und dann zurückgegeben, da kein "echter"Reader
verfügbar war.
Unter der Annahme, dass die Ausnahme so früh gefasst werden musste, gab es mehrere Alternativen für die Rückgabe von null
:
- Es wäre möglich, eine
NullReader
Klasse zu implementieren. B. eine, bei der sich die Operationen der API so verhalten, als befände sich der Leser bereits an der Position "Dateiende". - Mit Java 8 könnte
getReader
alsOptional<Reader>
.
Fallstricke - Nicht geprüft, ob ein E / A-Datenstrom beim Schließen noch nicht einmal initialisiert wurde
Um Speicherverluste zu vermeiden, sollten Sie nicht vergessen, einen Eingabestrom oder einen Ausgabestrom zu schließen, dessen Job erledigt ist. Dies geschieht normalerweise mit einer try
catch
finally
Anweisung ohne den catch
Teil:
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
out.close();
}
}
Obwohl der obige Code unschuldig wirkt, weist er einen Fehler auf, der das Debuggen unmöglich machen kann. Wenn die Zeile , in der out
initialisiert ( out = new FileOutputStream(filename)
) eine Ausnahme auslöst, dann out
wird null
, wenn out.close()
ausgeführt wird, was zu einem heftigen NullPointerException
!
Um dies zu verhindern, stellen Sie einfach sicher, dass der Stream nicht null
bevor Sie versuchen, ihn zu schließen.
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
if (out != null)
out.close();
}
}
Ein noch besserer Ansatz ist es try
-with-resources zu try
, da der Stream automatisch mit einer Wahrscheinlichkeit von 0 geschlossen wird, um eine NPE zu werfen, ohne dass ein finally
Block erforderlich ist.
void writeNullBytesToAFile(int count, String filename) throws IOException {
try (FileOutputStream out = new FileOutputStream(filename)) {
for(; count > 0; count--)
out.write(0);
}
}
Pitfall - Verwenden der "Yoda-Notation", um NullPointerException zu vermeiden
Eine Menge von Beispielcode, der in StackOverflow veröffentlicht wird, enthält folgende Ausschnitte:
if ("A".equals(someString)) {
// do something
}
Dies "verhindert" oder "vermeidet" eine mögliche NullPointerException
für den Fall, dass someString
null
. Darüber hinaus ist das fraglich
"A".equals(someString)
ist besser als:
someString != null && someString.equals("A")
(Es ist kürzer und kann unter Umständen effizienter sein. Wie wir jedoch weiter unten argumentieren, könnte die Prägnanz negativ sein.)
Die eigentliche Fallstricke verwendet jedoch den Yoda-Test , um NullPointerExceptions
aus Gewohnheit zu vermeiden .
Wenn Sie "A".equals(someString)
schreiben, "A".equals(someString)
Sie tatsächlich den "Fall" gut, in dem someString
zufällig null
. Ein anderes Beispiel ( Pitfall - "Making good" unerwartete Nullen ) erklärt jedoch, dass "gute" null
aus verschiedenen Gründen schädlich sein können.
Dies bedeutet, dass Yoda-Bedingungen keine "Best Practice" sind 1 . Wenn keine null
erwartet wird, ist es besser, die NullPointerException
passieren zu lassen, damit ein Unit-Test-Fehler (oder ein Fehlerbericht) auftreten kann. Auf diese Weise können Sie den Fehler finden und beheben, durch den die unerwartete / unerwünschte null
angezeigt wurde.
Yoda-Bedingungen sollten nur in Fällen verwendet werden, in denen die null
erwartet wird, da das von Ihnen getestete Objekt von einer API stammt , die als null
. Und es könnte besser sein, eine der weniger hübschen Methoden zu verwenden, um den Test auszudrücken, da dies den null
für jemanden hervorhebt, der Ihren Code überprüft.
1 - Laut Wikipedia : "Beste Kodierungspraktiken sind eine Reihe informeller Regeln, die die Softwareentwicklungsgemeinschaft im Laufe der Zeit gelernt hat und die dazu beitragen kann, die Qualität der Software zu verbessern." . Die Verwendung der Yoda-Notation erreicht dies nicht. In vielen Situationen wird der Code dadurch schlechter.