Warum explizit eine NullPointerException werfen, anstatt es auf natürliche Weise geschehen zu lassen?

Lesezeit: 10 Minuten

Benutzer-Avatar
LiJiaming

Beim Lesen des JDK-Quellcodes finde ich es üblich, dass der Autor die Parameter überprüft, ob sie null sind, und dann manuell eine neue NullPointerException() auslöst. Warum tun sie es? Ich denke, das ist nicht nötig, 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;
}

  • Ein wichtiger Punkt beim Codieren ist die Absicht

    – Gruseliger Wombat

    12. Mai 2017 um 2:57 Uhr

  • Dies ist eine sehr gute Frage für Ihre erste Frage! Ich habe einige kleinere Änderungen vorgenommen; Ich hoffe, Sie haben nichts dagegen. 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 Konrad

    12. Mai 2017 um 3:29 Uhr

  • Ich bin C # die Konvention ist zu erhöhen ArgumentNullException in solchen Fällen (anstatt NullReferenceException) – es ist eigentlich eine wirklich gute Frage, warum Sie das ansprechen würden NullPointerException explizit hier (und nicht anders).

    – EJoshuaS – Stehen Sie mit der Ukraine

    12. Mai 2017 um 3:44 Uhr


  • @EJoshuaS Es ist eine alte Debatte, ob man werfen soll IllegalArgumentException oder NullPointerException für ein Nullargument. Die JDK-Konvention ist die letztere.

    – Schmosel

    12. Mai 2017 um 3:47 Uhr

  • Das WIRKLICHE Problem ist, dass sie einen Fehler ausgeben und verwerfen Sie alle Informationen, die zu diesem Fehler geführt haben. Scheint dies Ist der tatsächlich Quellcode. Nicht einmal eine einfache blutige String-Nachricht. Traurig.

    – Martin B

    12. Mai 2017 um 8:59 Uhr

Es gibt eine Reihe von Gründen, die in den Sinn kommen, von denen einige eng miteinander verbunden sind:

Ausfallschnell: Wenn es scheitern soll, scheitern Sie besser früher als später. Dadurch können Probleme näher an ihrer Quelle erfasst werden, wodurch sie leichter identifiziert und behoben werden können. Es vermeidet auch die Verschwendung von CPU-Zyklen für Code, der zwangsläufig fehlschlägt.

Absicht: Das explizite Auslösen der Ausnahme macht den Betreuern klar, dass der Fehler absichtlich da ist und der Autor sich der Konsequenzen bewusst war.

Konsistenz: Wenn zugelassen würde, dass der Fehler auf natürliche Weise auftritt, tritt er möglicherweise nicht in jedem Szenario auf. Wenn zum Beispiel keine Zuordnung gefunden wird, remappingFunction niemals verwendet und die Ausnahme nicht ausgelöst. Die Validierung der Eingabe im Voraus ermöglicht ein deterministischeres und klareres Verhalten Dokumentation.

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

  • Außerdem: Auf diese Weise ist der Ort, an dem die Ausnahme ausgelöst wird, an genau eine Variable gebunden, die überprüft wird. Ohne sie könnte die Ausnahme darauf zurückzuführen sein, dass eine von mehreren Variablen null ist.

    – Jens Schauder

    12. Mai 2017 um 6:44 Uhr

  • Eine andere: Wenn Sie darauf warten, dass NPE auf natürliche Weise eintritt, hat möglicherweise bereits ein Zwischencode den Zustand Ihres Programms durch Nebeneffekte geändert.

    – Thomas

    12. Mai 2017 um 10:37 Uhr

  • Während dieses Snippet dies nicht tut, könnten Sie die verwenden new NullPointerException(message) Konstruktor, um zu verdeutlichen, was null ist. Gut für Leute, die keinen Zugriff auf Ihren Quellcode haben. Sie haben dies sogar zu einem Einzeiler in JDK 8 mit dem gemacht Objects.requireNonNull(object, message) Utility-Methode.

    – Robin

    12. Mai 2017 um 13:05 Uhr

  • Der FAILURE sollte in der Nähe des FAULT sein. „Fail Fast“ ist mehr als eine Faustregel. Wann würden Sie dieses Verhalten nicht wollen? Jedes andere Verhalten bedeutet, dass Sie Fehler verbergen. Es gibt „FAULTS“ und „FAILURES“. Der FAILURE ist, wenn dieses Programm einen NULL-Zeiger verdaut und abstürzt. Aber diese Codezeile ist nicht dort, wo FAULT liegt. Die NULL kam von irgendwoher – ein Methodenargument. Wer hat dieses Argument bestanden? Aus einer Codezeile, die auf eine lokale Variable verweist. Wo hat das… Sehen Sie? Das ist Scheiße. Wessen Verantwortung sollte es gewesen sein, zu sehen, dass ein schlechter Wert gespeichert wird? Ihr Programm sollte dann abgestürzt sein.

    – Noah Spurrier

    12. Mai 2017 um 16:19 Uhr

  • @Thomas Guter Punkt. Shmosel: Thomas’ Punkt mag im Fail-Fast-Punkt impliziert sein, aber er ist irgendwie begraben. Es ist ein Konzept, das so wichtig ist, dass es einen eigenen Namen hat: Fehler Atomizität. Siehe Bloch, Effektives Java, Item 46. Es hat eine stärkere Semantik als Fail-Fast. Ich würde vorschlagen, es in einem separaten Punkt anzusprechen. Insgesamt eine hervorragende Antwort, übrigens. +1

    – Stuart Marks

    13. Mai 2017 um 18:31 Uhr

Benutzer-Avatar
David Konrad

Es dient der Klarheit, Einheitlichkeit und um zu verhindern, dass zusätzliche, unnötige Arbeit geleistet wird.

Überlegen Sie, was passieren würde, wenn am Anfang der Methode keine Schutzklausel vorhanden wäre. Es würde immer anrufen hash(key) und getNode(hash, key) sogar wenn null war für die übergeben worden remappingFunction bevor die NPE geworfen wurde.

Noch schlimmer, wenn die if Zustand ist false dann nehmen wir die else Zweig, der die nicht verwendet remappingFunction überhaupt, was bedeutet, dass die Methode NPE nicht immer auslöst, wenn a null ist bestanden; ob dies der Fall ist, hängt vom Zustand der Karte ab.

Beide Szenarien sind schlecht. Wenn null ist kein gültiger Wert für remappingFunction Die Methode sollte unabhängig vom internen Zustand des Objekts zum Zeitpunkt des Aufrufs konsistent eine Ausnahme auslösen, und zwar ohne unnötige Arbeit, die sinnlos ist, da sie nur auslösen wird. Schließlich ist es ein gutes Prinzip für sauberen, klaren Code, den Wächter ganz vorne zu haben, damit jeder, der den Quellcode überprüft, leicht erkennen kann, dass er dies tun wird.

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. Die Durchführung der Überprüfung zu Beginn stellt sicher, dass sie definitiv durchgeführt wird.

Benutzer-Avatar
Stefan C

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

Leistung: Es kann / gab Leistungsvorteile (auf einigen JVMs), die NPE explizit zu werfen, anstatt die JVM dies tun zu lassen.

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

Ein expliziter Test für null im Code würde den SIGSEGV-Leistungseinbruch in einem Szenario vermeiden, in dem NPEs häufig vorkamen.

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


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

Benutzer-Avatar
EJoshuaS – Stehen Sie mit der Ukraine

Abgesehen von dem, was andere Leute darauf hingewiesen haben, ist es erwähnenswert, die Rolle der Konvention hier zu erwähnen. In C# haben Sie zum Beispiel auch die gleiche Konvention, in Fällen wie diesem explizit eine Ausnahme auszulösen, aber es ist speziell eine ArgumentNullException, was etwas spezifischer ist. (Die C#-Konvention ist das NullReferenceException stets stellt einen Fehler irgendeiner Art dar – ganz einfach sollte es nicht sein je passieren im Produktionscode; gewährt, ArgumentNullException normalerweise auch, aber es könnte ein Fehler sein, eher in Richtung “Sie verstehen nicht, wie man die Bibliothek richtig verwendet” Art von Fehler).

Also im Grunde in C# NullReferenceException bedeutet, dass Ihr Programm tatsächlich versucht hat, es zu verwenden, während ArgumentNullException es bedeutet, dass es erkannt hat, dass der Wert falsch war, und sich nicht einmal die Mühe gemacht hat, zu versuchen, ihn zu verwenden. Die Auswirkungen können tatsächlich unterschiedlich sein (je nach den Umständen), weil ArgumentNullException bedeutet, dass die fragliche Methode noch keine Nebenwirkungen hatte (da sie die Methodenvoraussetzungen nicht erfüllt hat).

Übrigens, wenn Sie so etwas anheben ArgumentNullException oder IllegalArgumentExceptiondas ist ein Teil des Punktes bei der Überprüfung: Sie möchten eine andere Ausnahme als die, die Sie “normalerweise” erhalten würden.

In jedem Fall verstärkt das explizite Auslösen der Ausnahme die bewährte Vorgehensweise, die Vorbedingungen und erwarteten Argumente Ihrer Methode explizit anzugeben, wodurch der Code einfacher zu lesen, zu verwenden und zu warten ist. Wenn Sie es nicht explizit überprüft haben nullIch weiß nicht, ob es daran liegt, dass du dachtest, dass niemand jemals an einem vorbeikommen würde null Argument, Sie zählen es sowieso, um die Ausnahme auszulösen, oder Sie haben einfach vergessen, danach zu suchen.

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

Benutzer-Avatar
cmaster – monica wieder einsetzen

Es verwandelt einen scheinbar unberechenbaren Fehlerzustand in einen klaren Vertragsbruch: Die Funktion hat einige Voraussetzungen, um korrekt zu arbeiten, also prüft sie diese vorher und erzwingt ihre Erfüllung.

Der Effekt ist, dass Sie nicht debuggen müssen computeIfPresent() wenn Sie die Ausnahme davon bekommen. Sobald Sie sehen, dass die Ausnahme von der Vorbedingungsprüfung stammt, wissen Sie, dass Sie die Funktion mit einem ungültigen Argument aufgerufen haben. Wenn das Häkchen nicht vorhanden wäre, müssten Sie die Möglichkeit ausschließen, dass sich darin ein Fehler befindet computeIfPresent() selbst, was dazu führt, dass die Ausnahme ausgelöst wird.

Offensichtlich das Generikum werfen NullPointerException ist eine wirklich schlechte Wahl, da es an und für sich keine Vertragsverletzung signalisiert. IllegalArgumentException wäre die bessere Wahl.


Randnotiz:
Ich weiß nicht, ob Java dies zulässt (ich bezweifle es), aber C/C++-Programmierer verwenden eine assert() in diesem Fall, was für das Debugging wesentlich besser ist: Es weist das Programm an, sofort und so hart wie möglich abzustürzen, wenn die bereitgestellte Bedingung als falsch ausgewertet wird. Also, wenn du rennst

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

    ...
}

unter einem Debugger, und etwas passiert NULL in jedes Argument, würde das Programm direkt an der Zeile anhalten, die angibt, um welches Argument es sich handelt NULLund Sie könnten alle lokalen Variablen des gesamten Aufrufstapels in Ruhe untersuchen.

Benutzer-Avatar
Arenim

Es ist, weil es dafür möglich ist nicht natürlich geschehen. Sehen wir uns einen Codeabschnitt 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, zu faul zu sein, mir ein perfektes Beispiel vorzustellen.)

Situation wann c wird nicht wirklich verwendet und NullPointerException wird nicht geworfen, auch wenn c == null ist möglich.

In komplizierteren Situationen wird es sehr schwierig, solche Fälle aufzuspüren. Deshalb allgemeine Überprüfung wie if (c == null) throw new NullPointerException() ist besser.

  • Ein Stück Code, das ohne Datenbankverbindung funktioniert, wenn es nicht wirklich benötigt wird, ist wohl eine gute Sache, und der Code, der eine Verbindung zu einer Datenbank herstellt, nur um zu sehen, ob dies nicht möglich ist, ist normalerweise ziemlich ärgerlich.

    – Dmitri Grigorjew

    15. Mai 2017 um 10:48 Uhr

1352050cookie-checkWarum explizit eine NullPointerException werfen, anstatt es auf natürliche Weise geschehen zu lassen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy