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 einem synchronized 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 der Map API gibt eine null wenn Sie sie mit einem Schlüssel aufrufen, der keine Zuordnung hat.
  • Die getResource(path) und getResourceAsStream(path) in den ClassLoader und Class APIs geben null wenn die Ressource nicht gefunden werden kann.
  • Die get() -Methode in der Reference API gibt null wenn der Garbage Collector die Referenz gelöscht hat.
  • Verschiedene getXxxx Methoden in den Java EE-Servlet-APIs geben null 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 und d primitive Wrapper sein müssen, sollten wir uns entweder nicht auf die Standardinitialisierung verlassen oder auf null testen. Für ersteres gilt der richtige Ansatz, es sei denn, es gibt eine eindeutige Bedeutung für die Felder im null .

  • 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:

  1. Die IOException wurde zu früh aufgefangen.
  2. Aufgrund des Aufbaus dieses Codes besteht die Gefahr, dass eine Ressource ausläuft.
  3. 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 :

  1. 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".
  2. Mit Java 8 könnte getReader als Optional<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.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow