Warum explizit eine NullPointerException auslösen, anstatt dies auf natürliche Weise geschehen zu lassen?

182

Beim Lesen von JDK-Quellcode wird häufig festgestellt, dass der Autor die Parameter überprüft, wenn sie null sind, und dann manuell eine neue NullPointerException () auslöst. Warum machen sie das? Ich denke, es ist nicht nötig, dies zu tun, da es eine neue NullPointerException () auslöst, wenn es eine Methode aufruft. (Hier ist zum Beispiel ein Quellcode von HashMap :)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}
LiJiaming
quelle
32
Ein wichtiger Punkt bei der Codierung ist die Absicht
Scary Wombat
19
Dies ist eine sehr gute Frage für Ihre erste Frage! Ich habe einige kleinere Änderungen vorgenommen. Ich hoffe es macht dir nichts aus. Ich habe auch das Dankeschön und den Hinweis entfernt, dass es Ihre erste Frage ist, da so etwas normalerweise nicht Teil von SO-Fragen ist.
David Conrad
11
Ich bin C #, die Konvention soll ArgumentNullExceptionin solchen Fällen (und nicht NullReferenceException) ansprechen - es ist eigentlich eine wirklich gute Frage, warum Sie das NullPointerExceptionhier explizit ansprechen würden (anstatt eine andere).
EJoshuaS - Wiedereinsetzung Monica
21
@EJoshuaS Es ist eine alte Debatte, ob man ein Null-Argument wirft IllegalArgumentExceptionoder nicht NullPointerException. Die JDK-Konvention ist die letztere.
Shmosel
33
Das WIRKLICHE Problem ist, dass sie einen Fehler auslösen und alle Informationen wegwerfen, die zu diesem Fehler geführt haben . Dies scheint der eigentliche Quellcode zu sein. Nicht einmal eine einfache blutige String-Nachricht. Traurig.
Martin Ba

Antworten:

254

Es gibt eine Reihe von Gründen, von denen einige eng miteinander verbunden sind:

Fail-Fast: Wenn es fehlschlägt, ist es am besten, eher früher als später zu scheitern. Auf diese Weise können Probleme näher an ihrer Quelle erkannt werden, sodass sie leichter identifiziert und behoben werden können. Außerdem wird vermieden, dass CPU-Zyklen für Code verschwendet werden, der zum Scheitern verurteilt ist.

Absicht: Durch das explizite Auslösen der Ausnahme wird den Betreuern klar gemacht, dass der Fehler absichtlich vorliegt und der Autor sich der Konsequenzen bewusst war.

Konsistenz: Wenn der Fehler auf natürliche Weise auftreten darf, tritt er möglicherweise nicht in jedem Szenario auf. Wenn beispielsweise keine Zuordnung gefunden wird, wird remappingFunctiondiese niemals verwendet und die Ausnahme wird nicht ausgelöst. Die vorherige Validierung der Eingabe ermöglicht ein deterministischeres Verhalten und eine klarere Dokumentation .

Stabilität: Der Code entwickelt sich im Laufe der Zeit. Code, der auf eine Ausnahme stößt, kann nach einigem Refactoring natürlich aufhören oder dies unter anderen Umständen tun. Wenn Sie es explizit werfen, ist es weniger wahrscheinlich, dass sich das Verhalten versehentlich ändert.

shmosel
quelle
14
Außerdem: Auf diese Weise wird der Speicherort, von dem aus die Ausnahme ausgelöst wird, an genau eine Variable gebunden, die überprüft wird. Ohne sie kann die Ausnahme darauf zurückzuführen sein, dass eine von mehreren Variablen null ist.
Jens Schauder
45
Eine andere: Wenn Sie darauf warten, dass NPE auf natürliche Weise auftritt, hat möglicherweise ein Zwischencode den Status Ihres Programms bereits durch Nebenwirkungen geändert.
Thomas
6
Während dieses Snippet dies nicht tut, können Sie den new NullPointerException(message)Konstruktor verwenden, um zu klären, was null ist. Gut für Leute, die keinen Zugriff auf Ihren Quellcode haben. Sie machten dies sogar zu einem Einzeiler in JDK 8 mit der Objects.requireNonNull(object, message)Utility-Methode.
Robin
3
Der FEHLER sollte sich in der Nähe des FEHLERS befinden. "Fail Fast" ist mehr als eine Faustregel. Wann möchten Sie dieses Verhalten nicht? Jedes andere Verhalten bedeutet, dass Sie Fehler verbergen. Es gibt "FEHLER" und "FEHLER". Die FAILURE ist, wenn dieses Programm einen NULL-Zeiger verdaut und abstürzt. Aber in dieser Codezeile liegt FAULT nicht. Der NULL kam von irgendwoher - ein Methodenargument. Wer hat dieses Argument bestanden? Aus einer Codezeile, die auf eine lokale Variable verweist. Wo hat das ... Siehst du? Das ist Scheiße. Wessen Verantwortung hätte es sein müssen, dass ein schlechter Wert gespeichert wird? Ihr Programm sollte dann abgestürzt sein.
Noah Spurrier
4
@ Thomas Guter Punkt. Shmosel: Thomas 'Punkt mag im Fail-Fast-Punkt impliziert sein, aber er ist irgendwie begraben. Es ist ein Konzept, das wichtig genug ist, um einen eigenen Namen zu haben: Versagensatomizität . Siehe Bloch, Effective Java , Punkt 46. Es hat eine stärkere Semantik als Fail-Fast. Ich würde vorschlagen, es an einer anderen Stelle zu erwähnen. Insgesamt eine ausgezeichnete Antwort, übrigens. +1
Stuart Marks
40

Dies dient der Klarheit, Konsistenz und um zu verhindern, dass zusätzliche, unnötige Arbeiten ausgeführt werden.

Überlegen Sie, was passieren würde, wenn oben in der Methode keine Schutzklausel vorhanden wäre. Es würde immer anrufen hash(key)und getNode(hash, key)selbst wenn nulles für das übergeben worden war, remappingFunctionbevor die NPE geworfen wurde.

Schlimmer noch, wenn die ifBedingung erfüllt ist false, nehmen wir den elseZweig, der das überhaupt nicht verwendet remappingFunction, was bedeutet, dass die Methode nicht immer NPE auslöst, wenn a übergeben nullwird. ob dies der Fall ist, hängt vom Status der Karte ab.

Beide Szenarien sind schlecht. Wenn dies nullkein gültiger Wert für remappingFunctiondie Methode ist, sollte unabhängig vom internen Status des Objekts zum Zeitpunkt des Aufrufs konsistent eine Ausnahme ausgelöst werden, und dies sollte ohne unnötige Arbeit geschehen, die sinnlos ist, da sie nur ausgelöst wird. Schließlich ist es ein gutes Prinzip für sauberen, klaren Code, den Wachmann ganz vorne zu haben, damit jeder, der den Quellcode überprüft, leicht erkennen kann, dass dies der Fall ist.

Selbst wenn die Ausnahme derzeit von jedem Codezweig ausgelöst würde, ist es möglich, dass eine zukünftige Überarbeitung des Codes dies ändern würde. Wenn Sie die Prüfung zu Beginn durchführen, wird sichergestellt, dass sie definitiv durchgeführt wird.

David Conrad
quelle
25

Zusätzlich zu den Gründen, die in der hervorragenden Antwort von @ shmosel aufgeführt sind ...

Leistung: Es kann / gab Leistungsvorteile (bei einigen JVMs), die NPE explizit zu werfen, anstatt sie von der JVM ausführen zu lassen.

Dies hängt von der Strategie ab, die der Java-Interpreter und der JIT-Compiler verfolgen, um die Dereferenzierung von Nullzeigern zu erkennen. Eine Strategie besteht darin, nicht auf Null zu testen, sondern das SIGSEGV abzufangen, das auftritt, wenn ein Befehl versucht, auf die Adresse 0 zuzugreifen. Dies ist der schnellste Ansatz, wenn die Referenz immer gültig ist, im NPE-Fall jedoch teuer .

Ein expliziter Test nullim Code würde den SIGSEGV-Leistungseinbruch in einem Szenario vermeiden, in dem NPEs häufig waren.

(Ich bezweifle, dass dies eine lohnende Mikrooptimierung in einer modernen JVM wäre, aber es könnte in der Vergangenheit gewesen sein.)


Kompatibilität: Der wahrscheinliche Grund dafür, dass die Ausnahme keine Meldung enthält, ist die Kompatibilität mit NPEs, die von der JVM selbst ausgelöst werden. In einer kompatiblen Java-Implementierung hat eine von der JVM ausgelöste NPE eine nullNachricht. (Android Java ist anders.)

Stephen C.
quelle
20

Abgesehen von dem, worauf andere hingewiesen haben, ist es wert, die Rolle der Konvention hier zu erwähnen. In C # haben Sie beispielsweise auch die gleiche Konvention, in solchen Fällen explizit eine Ausnahme auszulösen, aber es ist speziell eine ArgumentNullException, die etwas spezifischer ist. (Die C # -Konvention besagt, dass es sich NullReferenceException immer um einen Fehler handelt - ganz einfach, es sollte niemals im Produktionscode auftreten; dies ist ArgumentNullExceptionnormalerweise auch der Fall, aber es könnte sich eher um einen Fehler im Sinne von "Sie tun es nicht" handeln verstehen, wie man die Bibliothek richtig benutzt "Art von Fehler).

Im Grunde NullReferenceExceptionbedeutet C # , dass Ihr Programm tatsächlich versucht hat, es zu verwenden, während ArgumentNullExceptiones bedeutet, dass es erkannt hat, dass der Wert falsch war, und dass es sich nicht einmal die Mühe gemacht hat, ihn zu verwenden. Die Auswirkungen können tatsächlich unterschiedlich sein (abhängig von den Umständen), da ArgumentNullExceptiondie betreffende Methode noch keine Nebenwirkungen hatte (da die Methodenvoraussetzungen nicht erfüllt wurden).

Übrigens, wenn Sie so etwas wie ArgumentNullExceptionoder IllegalArgumentExceptionansprechen, ist dies ein Teil des Punktes, an dem Sie die Prüfung durchführen: Sie möchten eine andere Ausnahme als "normalerweise".

In beiden Fällen wird durch das explizite Auslösen der Ausnahme die bewährte Methode verstärkt, die Voraussetzungen und erwarteten Argumente Ihrer Methode explizit anzugeben, wodurch der Code leichter zu lesen, zu verwenden und zu warten ist. Wenn Sie nicht explizit nachgesehen haben null, weiß ich nicht, ob es daran liegt, dass Sie dachten, dass niemand jemals ein nullArgument übergeben würde, Sie zählen es, um die Ausnahme trotzdem auszulösen, oder Sie haben einfach vergessen, danach zu suchen.

EJoshuaS - Monica wieder einsetzen
quelle
4
+1 für den mittleren Absatz. Ich würde argumentieren, dass der fragliche Code 'neue IllegalArgumentException auslösen sollte ("remappingFunction kann nicht null sein");' Auf diese Weise wird sofort klar, was falsch war. Die gezeigte NPE ist etwas mehrdeutig.
Chris Parker
1
@ChrisParker Früher war ich der gleichen Meinung, aber es stellt sich heraus, dass NullPointerException ein Nullargument kennzeichnen soll, das an eine Methode übergeben wird, die ein Nicht-Null-Argument erwartet, und zusätzlich eine Laufzeitantwort auf einen Versuch ist, Null zu dereferenzieren. Aus dem Javadoc: "Anwendungen sollten Instanzen dieser Klasse auslösen, um auf andere illegale Verwendungen des nullObjekts hinzuweisen ." Ich bin nicht verrückt danach, aber das scheint das beabsichtigte Design zu sein.
VGR
1
Ich stimme zu, @ChrisParker - Ich denke, dass diese Ausnahme spezifischer ist (da der Code nie versucht hat, etwas mit dem Wert zu tun, hat er sofort erkannt, dass er ihn nicht verwenden sollte). Ich mag die C # -Konvention in diesem Fall. Die C # -Konvention besagt, dass NullReferenceException(äquivalent zu NullPointerException) bedeutet, dass Ihr Code tatsächlich versucht hat, ihn zu verwenden (was immer ein Fehler ist - es sollte niemals im Produktionscode vorkommen), vs. "Ich weiß, dass das Argument falsch ist, also habe ich es nicht einmal getan." versuche es zu benutzen. " Es gibt auch ArgumentException(was bedeutet, dass das Argument aus einem anderen Grund falsch war).
EJoshuaS
2
Ich werde so viel sagen, ich werfe IMMER eine IllegalArgumentException wie beschrieben. Ich fühle mich immer wohl, wenn ich Konventionen missachte, wenn ich das Gefühl habe, dass die Konvention dumm ist.
Chris Parker
1
@PieterGeerkens - ja, weil die Zeile 35 von NullPointerException SO viel besser ist als die Zeile 35 von IllegalArgumentException ("Funktion kann nicht null sein"). Ernsthaft?
Chris Parker
12

Es ist so, dass Sie die Ausnahme erhalten, sobald Sie den Fehler begangen haben, und nicht später, wenn Sie die Karte verwenden und nicht verstehen, warum es passiert ist.

Marquis von Lorne
quelle
9

Es verwandelt eine scheinbar unberechenbare Fehlerbedingung in eine eindeutige Vertragsverletzung: Die Funktion hat einige Voraussetzungen für ein korrektes Funktionieren, sodass sie im Voraus überprüft und die Einhaltung erzwungen wird.

Der Effekt ist, dass Sie nicht debuggen müssen, computeIfPresent()wenn Sie die Ausnahme daraus ziehen. Sobald Sie sehen, dass die Ausnahme von der Vorbedingungsprüfung stammt, wissen Sie, dass Sie die Funktion mit einem unzulässigen Argument aufgerufen haben. Wenn die Prüfung nicht vorhanden wäre, müssten Sie die Möglichkeit ausschließen, dass ein Fehler in sich computeIfPresent()selbst vorliegt , der dazu führt, dass die Ausnahme ausgelöst wird.

Offensichtlich ist das Werfen des Generikums NullPointerExceptioneine wirklich schlechte Wahl, da es an und für sich keine Vertragsverletzung signalisiert. IllegalArgumentExceptionwäre eine bessere Wahl.


Nebenbemerkung:
Ich weiß nicht, ob Java dies zulässt (ich bezweifle es), aber C / C ++ - Programmierer verwenden assert()in diesem Fall eine, die für das Debuggen wesentlich besser ist: Sie weist das Programm an, sofort und so hart wie möglich abzustürzen, falls dies bereitgestellt wird Bedingung zu falsch bewerten. Also, wenn du gelaufen bist

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

Unter einem Debugger und etwas, das NULLin eines der beiden Argumente übergeht, stoppt das Programm direkt an der Zeile, die NULLangibt , um welches Argument es sich handelt , und Sie können alle lokalen Variablen des gesamten Aufrufstapels nach Belieben untersuchen.

cmaster - Monica wieder einsetzen
quelle
1
assert something != null;Dies erfordert jedoch das -assertionsFlag, wenn die App ausgeführt wird. Wenn das -assertionsFlag nicht vorhanden ist,
Zoe
Ich stimme zu, deshalb bevorzuge ich hier die C # -Konvention - Nullreferenz, ungültiges Argument und Nullargument implizieren im Allgemeinen einen Fehler, aber sie implizieren verschiedene Arten von Fehlern. "Sie versuchen, eine Nullreferenz zu verwenden" unterscheidet sich häufig stark von "Sie missbrauchen die Bibliothek".
EJoshuaS
7

Es ist, weil es möglich ist, dass es nicht natürlich passiert. Sehen wir uns einen Code wie diesen an:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(Natürlich gibt es in diesem Beispiel zahlreiche Probleme mit der Codequalität. Tut mir leid, dass ich mir ein perfektes Beispiel vorstellen kann.)

Situation, in cder nicht tatsächlich verwendet wird und NullPointerExceptionnicht geworfen wird, auch wenn dies c == nullmöglich ist.

In komplizierteren Situationen wird es sehr einfach, solche Fälle aufzuspüren. Aus diesem Grund ist eine allgemeine Überprüfung if (c == null) throw new NullPointerException()besser.

Arenim
quelle
Wahrscheinlich ist ein Code, der ohne Datenbankverbindung funktioniert, wenn er nicht wirklich benötigt wird, eine gute Sache, und der Code, der eine Verbindung zu einer Datenbank herstellt, um zu sehen, ob dies nicht möglich ist, ist normalerweise ziemlich ärgerlich.
Dmitry Grigoryev
5

Es ist beabsichtigt, weiteren Schaden zu schützen oder in einen inkonsistenten Zustand zu gelangen.

Fairoz
quelle
1

Neben all den anderen hervorragenden Antworten hier möchte ich auch einige Fälle hinzufügen.

Sie können eine Nachricht hinzufügen, wenn Sie eine eigene Ausnahme erstellen

Wenn Sie Ihre eigenen werfen NullPointerException, können Sie eine Nachricht hinzufügen (was Sie auf jeden Fall sollten!)

Die Standardnachricht ist beispielsweise eine nullvon new NullPointerException()und alle Methoden, die sie verwenden Objects.requireNonNull. Wenn Sie diese Null drucken, kann sie sogar in eine leere Zeichenfolge übersetzt werden ...

Ein bisschen kurz und nicht informativ ...

Der Stack-Trace gibt viele Informationen, aber damit der Benutzer weiß, was null war, muss er den Code ausgraben und die genaue Zeile anzeigen.

Stellen Sie sich nun vor, dass NPE verpackt und über das Internet gesendet wird, z. B. als Nachricht in einem Webdienstfehler, möglicherweise zwischen verschiedenen Abteilungen oder sogar Organisationen. Im schlimmsten Fall kann niemand herausfinden, wofür null...

Verkettete Methodenaufrufe halten Sie auf dem Laufenden

Eine Ausnahme sagt Ihnen nur, in welcher Zeile die Ausnahme aufgetreten ist. Betrachten Sie die folgende Zeile:

repository.getService(someObject.someMethod());

Wenn Sie eine NPE erhalten und diese auf diese Zeile zeigt, welche von repositoryund someObjectwar null?

Stattdessen zeigt das Überprüfen dieser Variablen, wenn Sie sie erhalten, zumindest auf eine Zeile, in der sie hoffentlich die einzige Variable sind, die behandelt wird. Und wie bereits erwähnt, noch besser, wenn Ihre Fehlermeldung den Namen der Variablen oder ähnliches enthält.

Fehler bei der Verarbeitung vieler Eingaben sollten identifizierende Informationen liefern

Stellen Sie sich vor, Ihr Programm verarbeitet eine Eingabedatei mit Tausenden von Zeilen und plötzlich gibt es eine NullPointerException. Sie sehen sich den Ort an und stellen fest, dass einige Eingaben falsch waren ... welche Eingaben? Sie benötigen weitere Informationen zur Zeilennummer, möglicherweise zur Spalte oder sogar zum gesamten Zeilentext, um zu verstehen, welche Zeile in dieser Datei korrigiert werden muss.

Erk
quelle