Vermeiden Sie die Prüfung auf Null-Anweisung in Java

Übersicht

Im Allgemeinen sind Nullvariablen, Referenzen und Sammlungen in Java-Code schwierig zu handhaben. Sie sind nicht nur schwer zu identifizieren, sondern auch komplex zu handhaben.

Tatsächlich kann ein Fehler im Umgang mit null zur Kompilierungszeit nicht identifiziert werden und führt zur Laufzeit zu einer NullPointerException.

In diesem Tutorial werfen wir einen Blick auf die Notwendigkeit, in Java nach Null zu suchen, und verschiedene Alternativen, die uns helfen, Nullprüfungen in unserem Code zu vermeiden.

Further reading:

Using NullAway to Avoid NullPointerExceptions

Learn how to avoid NullPointerExceptions using NullAway.
Read more →

Spring Null-Safety Annotations

A quick and practical guide to null-safety annotations in Spring.
Read more →

Introduction to the Null Object Pattern

Learn about the Null Object Pattern and how to implement it in Java
Read more →

What Is NullPointerException?

Gemäß dem Javadoc für NullPointerException wird es ausgelöst, wenn eine Anwendung versucht, null in einem Fall zu verwenden, in dem ein Objekt erforderlich ist, z. B .:

  • Aufrufen einer Instanzmethode eines Nullobjekts
  • Zugreifen auf ein Feld eines Nullobjekts oder Ändern eines Felds
  • Die Länge von null nehmen, als wäre es ein Array
  • Auf die Länge von null zugreifen oder diese ändern, als wäre es ein Array
  • enn es ein werfbarer Wert wäre

Lassen Sie uns schnell ein paar Beispiele für den Java-Code sehen, der diese Ausnahme verursacht:

public void doSomething() { String result = doSomethingElse(); if (result.equalsIgnoreCase("Success")) // success }}private String doSomethingElse() { return null;}

Hier haben wir versucht, einen Methodenaufruf für eine Nullreferenz aufzurufen. Dies würde zu einer NullPointerException führen.

Ein weiteres häufiges Beispiel ist, wenn wir versuchen, auf ein Null-Array zuzugreifen:

public static void main(String args) { findMax(null);}private static void findMax(int arr) { int max = arr; //check other elements in loop}

Dies verursacht eine NullPointerException in Zeile 6.

Der Zugriff auf ein beliebiges Feld, eine Methode oder einen Index eines Nullobjekts verursacht daher eine NullPointerException, wie aus den obigen Beispielen hervorgeht.

Eine gängige Methode zur Vermeidung der NullPointerException besteht darin, nach null zu suchen:

public void doSomething() { String result = doSomethingElse(); if (result != null && result.equalsIgnoreCase("Success")) { // success } else // failure}private String doSomethingElse() { return null;}

In der realen Welt fällt es Programmierern schwer zu erkennen, welche Objekte null sein können. Eine aggressiv sichere Strategie könnte darin bestehen, null für jedes Objekt zu überprüfen. Dies verursacht jedoch viele redundante Nullprüfungen und macht unseren Code weniger lesbar.

In den nächsten Abschnitten werden wir einige der Alternativen in Java durchgehen, die eine solche Redundanz vermeiden.

Behandlung von Null durch den API-Vertrag

Wie im letzten Abschnitt erläutert, verursacht der Zugriff auf Methoden oder Variablen von Nullobjekten eine NullPointerException. Wir haben auch besprochen, dass durch eine Nullprüfung eines Objekts vor dem Zugriff die Möglichkeit einer NullPointerException ausgeschlossen wird.

Häufig gibt es jedoch APIs, die Nullwerte verarbeiten können. Zum Beispiel:

public void print(Object param) { System.out.println("Printing " + param);}public Object process() throws Exception { Object result = doSomething(); if (result == null) { throw new Exception("Processing fail. Got a null response"); } else { return result; }}

Der Methodenaufruf print() würde nur „null“ ausgeben, aber keine Ausnahme auslösen. In ähnlicher Weise würde process() niemals null in seiner Antwort zurückgeben. Es löst eher eine Ausnahme aus.

Für einen Client-Code, der auf die obigen APIs zugreift, ist also keine Nullprüfung erforderlich.

Solche APIs müssen dies jedoch in ihrem Vertrag explizit machen. Ein üblicher Ort für APIs, um einen solchen Vertrag zu veröffentlichen, ist das JavaDoc.

Dies gibt jedoch keinen klaren Hinweis auf den API-Vertrag und hängt daher von den Client-Code-Entwicklern ab, um deren Einhaltung sicherzustellen.

Im nächsten Abschnitt werden wir sehen, wie einige IDEs und andere Entwicklungstools Entwicklern dabei helfen.

API-Verträge automatisieren

4.1. Mit statischen Code-Analyse

Statische Code-Analyse-Tools helfen, die Code-Qualität zu einem großen Teil zu verbessern. Und ein paar solcher Tools ermöglichen es den Entwicklern auch, den Null-Vertrag beizubehalten. Ein Beispiel ist FindBugs.

FindBugs hilft bei der Verwaltung des Null-Vertrags durch die Annotationen @Nullable und @NonNull. Wir können diese Anmerkungen für jede Methode, jedes Feld, jede lokale Variable oder jeden Parameter verwenden. Dies macht es für den Client-Code explizit, ob der kommentierte Typ null sein kann oder nicht. Sehen wir uns ein Beispiel an:

public void accept(@Nonnull Object param) { System.out.println(param.toString());}

Hier macht @NonNull klar, dass das Argument nicht null sein kann. Wenn der Clientcode diese Methode aufruft, ohne das Argument auf null zu überprüfen, generiert FindBugs zur Kompilierungszeit eine Warnung.

4.2. IDE-Unterstützung verwenden

Entwickler verlassen sich im Allgemeinen auf IDEs, um Java-Code zu schreiben. Und Funktionen wie intelligente Codevervollständigung und nützliche Warnungen, z. B. wenn eine Variable möglicherweise nicht zugewiesen wurde, helfen sicherlich in hohem Maße.

Einige IDEs ermöglichen es Entwicklern auch, API-Verträge zu verwalten und damit die Notwendigkeit eines statischen Codeanalysetools zu beseitigen. IntelliJ IDEA stellt die Annotationen @NonNull und @Nullable . Um die Unterstützung für diese Anmerkungen in IntelliJ hinzuzufügen, müssen wir die folgende Maven-Abhängigkeit hinzufügen:

<dependency> <groupId>org.jetbrains</groupId> <artifactId>annotations</artifactId> <version>16.0.2</version></dependency>

Nun generiert IntelliJ eine Warnung, wenn die Nullprüfung fehlt, wie in unserem letzten Beispiel.

IntelliJ bietet auch eine Vertragsanmerkung für die Handhabung komplexer API-Verträge.

5. Assertions

Bisher haben wir nur darüber gesprochen, die Notwendigkeit von Nullprüfungen aus dem Client-Code zu entfernen. Dies ist jedoch in realen Anwendungen selten anwendbar.Nehmen wir nun an, wir arbeiten mit einer API, die keine Null-Parameter akzeptieren kann oder eine Null-Antwort zurückgeben kann, die vom Client verarbeitet werden muss. Dies macht es erforderlich, die Parameter oder die Antwort auf einen Nullwert zu überprüfen.

Hier können wir Java-Assertionen anstelle der traditionellen bedingten Null-Check-Anweisung verwenden:

public void accept(Object param){ assert param != null; doSomething(param);}

In Zeile 2 suchen wir nach einem Null-Parameter. Wenn die assertions aktiviert sind, würde dies zu einem AssertionError .

Obwohl es eine gute Möglichkeit ist, Vorbedingungen wie Nicht-Null-Parameter zu behaupten, hat dieser Ansatz zwei Hauptprobleme:

  1. Assertionen werden normalerweise in einer JVM deaktiviert
  2. Eine falsche Assertion führt zu einem ungeprüften Fehler, der nicht behebbar ist

Daher wird Programmierern nicht empfohlen, Assertionen zum Überprüfen von Bedingungen zu verwenden. In den folgenden Abschnitten werden andere Möglichkeiten zur Behandlung von Nullvalidierungen erläutert.

Vermeiden von Nullprüfungen durch Codierungspraktiken

6.1. Vorbedingungen

Normalerweise empfiehlt es sich, Code zu schreiben, der frühzeitig fehlschlägt. Wenn eine API mehrere Parameter akzeptiert, die nicht null sein dürfen, ist es daher besser, nach jedem Nicht-Null-Parameter als Voraussetzung für die API zu suchen.

Schauen wir uns zum Beispiel zwei Methoden an – eine, die früh fehlschlägt, und eine, die nicht funktioniert:

public void goodAccept(String one, String two, String three) { if (one == null || two == null || three == null) { throw new IllegalArgumentException(); } process(one); process(two); process(three);}public void badAccept(String one, String two, String three) { if (one == null) { throw new IllegalArgumentException(); } else { process(one); } if (two == null) { throw new IllegalArgumentException(); } else { process(two); } if (three == null) { throw new IllegalArgumentException(); } else { process(three); }}

Wir sollten goodAccept() eindeutig badAccept() vorziehen.

Alternativ können wir auch die Vorbedingungen von Guava zur Validierung von API-Parametern verwenden.

6.2. Verwenden von Primitiven anstelle von Wrapper-Klassen

Da null für Primitive wie int kein akzeptabler Wert ist, sollten wir sie nach Möglichkeit ihren Wrapper-Gegenstücken wie Integer vorziehen.

Betrachten Sie zwei Implementierungen einer Methode, die zwei ganze Zahlen summiert:

public static int primitiveSum(int a, int b) { return a + b;}public static Integer wrapperSum(Integer a, Integer b) { return a + b;}

Rufen wir nun diese APIs in unserem Client-Code auf:

int sum = primitiveSum(null, 2);

Dies würde zu einem Kompilierungsfehler führen, da null kein gültiger Wert für ein int ist.

Und wenn wir die API mit Wrapper-Klassen verwenden, erhalten wir eine NullPointerException:

assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));

Es gibt auch andere Faktoren für die Verwendung von Primitiven gegenüber Wrappern, wie wir in einem anderen Tutorial, Java Primitives versus Objects, behandelt haben.

6.3. Leere Sammlungen

Gelegentlich müssen wir eine Sammlung als Antwort von einer Methode zurückgeben. Für solche Methoden sollten wir immer versuchen, eine leere Sammlung anstelle von null zurückzugeben:

public List<String> names() { if (userExists()) { return Stream.of(readName()).collect(Collectors.toList()); } else { return Collections.emptyList(); }}

Daher haben wir vermieden, dass unser Client beim Aufrufen dieser Methode eine Nullprüfung durchführen muss.

Objekte verwenden

Java 7 hat die neue Objects API eingeführt. Diese API verfügt über mehrere statische Dienstprogrammmethoden, die viel redundanten Code entfernen. Schauen wir uns eine solche Methode an, requireNonNull():

public void accept(Object param) { Objects.requireNonNull(param); // doSomething()}

Testen wir nun die accept() -Methode:

assertThrows(NullPointerException.class, () -> accept(null));

Wenn also null als Argument übergeben wird, löst accept() eine NullPointerException aus.

Diese Klasse hat auch isNull() und NonNull() Methoden, die als Prädikate verwendet werden können, um ein Objekt auf null zu überprüfen.

Mit optionalem

8.1. Verwenden von orElseThrow

Java 8 hat eine neue optionale API in der Sprache eingeführt. Dies bietet einen besseren Vertrag für die Behandlung optionaler Werte im Vergleich zu null . Mal sehen, wie Optional die Notwendigkeit von Nullprüfungen beseitigt:

public Optional<Object> process(boolean processed) { String response = doSomething(processed); if (response == null) { return Optional.empty(); } return Optional.of(response);}private String doSomething(boolean processed) { if (processed) { return "passed"; } else { return null; }}

Durch die Rückgabe eines Optionalen, wie oben gezeigt, macht die process Methode dem Aufrufer klar, dass die Antwort leer sein kann und zur Kompilierungszeit behandelt werden muss.

Dadurch entfällt insbesondere die Notwendigkeit von Nullprüfungen im Client-Code. Eine leere Antwort kann mit dem deklarativen Stil der optionalen API unterschiedlich behandelt werden:

assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));

Darüber hinaus bietet es API-Entwicklern eine bessere Möglichkeit, den Clients anzuzeigen, dass eine API eine leere Antwort zurückgeben kann.

Obwohl wir die Notwendigkeit einer Nullprüfung für den Aufrufer dieser API beseitigt haben, haben wir sie verwendet, um eine leere Antwort zurückzugeben. Um dies zu vermeiden, stellt Optional eine ofNullable-Methode bereit, die ein Optional mit dem angegebenen Wert oder leer zurückgibt, wenn der Wert null ist:

public Optional<Object> process(boolean processed) { String response = doSomething(processed); return Optional.ofNullable(response);}

8.2. Verwenden von Optional mit Sammlungen

Beim Umgang mit leeren Sammlungen ist Optional praktisch:

public String findFirst() { return getList().stream() .findFirst() .orElse(DEFAULT_VALUE);}

Diese Funktion soll das erste Element einer Liste zurückgeben. Die FindFirst-Funktion der Stream-API gibt ein leeres Optional zurück, wenn keine Daten vorhanden sind. Hier haben wir OrElse verwendet, um stattdessen einen Standardwert bereitzustellen.

Auf diese Weise können wir entweder leere Listen oder Listen verarbeiten, die nach Verwendung der Filtermethode der Stream-Bibliothek keine Elemente mehr enthalten.

Alternativ können wir dem Client auch erlauben, zu entscheiden, wie er mit empty umgeht, indem er Optional von dieser Methode zurückgibt:

public Optional<String> findOptionalFirst() { return getList().stream() .findFirst();}

Wenn das Ergebnis von GetList leer ist, gibt diese Methode daher eine leere Liste an den Client zurück.

Die Verwendung von Optional with collections ermöglicht es uns, APIs zu entwerfen, die sicher sind, Nicht-Null-Werte zurückzugeben, wodurch explizite Nullprüfungen auf dem Client vermieden werden.

Es ist wichtig zu beachten, dass diese Implementierung davon abhängt, dass GetList nicht null zurückgibt. Wie wir jedoch im letzten Abschnitt besprochen haben, ist es oft besser, eine leere Liste als eine Null zurückzugeben.

8.3. Kombinieren von Optionals

Wenn wir anfangen, unsere Funktionen optional zurückzugeben, benötigen wir eine Möglichkeit, ihre Ergebnisse zu einem einzigen Wert zu kombinieren. Nehmen wir unser GetList Beispiel von früher. Was wäre, wenn es eine optionale Liste zurückgeben würde oder mit einer Methode umbrochen werden würde, die eine null mit Optional using ofNullable ?

Unsere FindFirst-Methode möchte ein optionales erstes Element einer optionalen Liste zurückgeben:

public Optional<String> optionalListFirst() { return getOptionalList() .flatMap(list -> list.stream().findFirst());}

Mit der flatMap-Funktion für das von getOptional zurückgegebene Optional können wir das Ergebnis eines inneren Ausdrucks entpacken, der Optional zurückgibt. Ohne flatMap wäre das Ergebnis Optional<Optional<String>> . Die flatMap-Operation wird nur ausgeführt, wenn das optionale Feld nicht leer ist.

Bibliotheken

9.1. Lombok verwenden

Lombok ist eine großartige Bibliothek, die die Menge an Boilerplate-Code in unseren Projekten reduziert. Es kommt mit einer Reihe von Anmerkungen, die an die Stelle von gemeinsamen Teilen des Codes treten, die wir oft selbst in Java-Anwendungen schreiben, wie Getter, Setter und toString (), um nur einige zu nennen.

Eine weitere Annotation ist @NonNull . Wenn also ein Projekt bereits Lombok verwendet, um Boilerplate-Code zu eliminieren, kann @NonNull die Notwendigkeit von Nullprüfungen ersetzen.

Bevor wir uns einige Beispiele ansehen, fügen wir eine Maven-Abhängigkeit für Lombok hinzu:

<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.6</version></dependency>

Jetzt können wir @NonNull überall dort verwenden, wo eine Nullprüfung erforderlich ist:

public void accept(@NonNull Object param){ System.out.println(param);}

Also haben wir einfach das Objekt kommentiert, für das die Nullprüfung erforderlich gewesen wäre, und Lombok generiert die kompilierte Klasse:

public void accept(@NonNull Object param) { if (param == null) { throw new NullPointerException("param"); } else { System.out.println(param); }}

Wenn param null ist, löst diese Methode eine NullPointerException aus. Die Methode muss dies in ihrem Vertrag explizit machen, und der Client-Code muss die Ausnahme behandeln.

9.2. Verwenden von StringUtils

Im Allgemeinen umfasst die Zeichenfolgenvalidierung zusätzlich zum Nullwert eine Überprüfung auf einen leeren Wert. Daher wäre eine übliche Validierungsanweisung:

public void accept(String param){ if (null != param && !param.isEmpty()) System.out.println(param);}

Dies wird schnell überflüssig, wenn wir mit vielen String-Typen umgehen müssen. Dies ist, wo StringUtils kommt praktisch. Bevor wir dies in Aktion sehen, fügen wir eine Maven-Abhängigkeit für commons-lang3 hinzu:

<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version></dependency>

Lassen Sie uns nun den obigen Code mit StringUtils umgestalten:

public void accept(String param) { if (StringUtils.isNotEmpty(param)) System.out.println(param);}

Also haben wir unsere Null- oder Leerprüfung durch eine statische Hilfsmethode IsNotEmpty() ersetzt. Diese API bietet weitere leistungsstarke Hilfsmethoden für die Handhabung gängiger String-Funktionen.

Fazit

In diesem Artikel haben wir uns die verschiedenen Gründe für NullPointerException angesehen und warum es schwer zu identifizieren ist. Dann haben wir verschiedene Möglichkeiten gesehen, die Redundanz im Code zu vermeiden, um mit Parametern, Rückgabetypen und anderen Variablen nach Null zu suchen.

Alle Beispiele sind auf GitHub verfügbar.

Erste Schritte mit Spring 5 und Spring Boot 2 durch den Learn Spring-Kurs:

>> SCHAUEN SIE SICH DEN KURS an



Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.