Vermeiden Sie es, in Java nach Null-Anweisungen zu suchen

1. Ü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 beim Umgang mit Null zur Kompilierungszeit nicht identifiziert werden und führt zur Laufzeit zu einer NullPointerException .

In diesem Tutorial werden wir einen Blick auf die Notwendigkeit , nehmen zu überprüfen null in Java und verschiedene Alternativen , die helfen , uns zu vermeiden null Kontrollen in unserem Code.

2. Was ist NullPointerException?

Laut der Javadoc for NullPointerException wird sie ausgelöst, wenn eine Anwendung versucht, null zu verwenden, wenn ein Objekt erforderlich ist, z.

  • Aufrufen einer Instanzmethode eines Null - Objekt
  • Zugreifen oder ein Feld eines modifizierenden null Objekt
  • Nehmen Sie die Länge von Null, als wäre es ein Array
  • Zugreifen auf oder Ändern der Slots von null, als wäre es ein Array
  • Null werfen, als wäre es ein Throwable- Wert

Lassen Sie uns schnell einige Beispiele für den Java-Code sehen, die diese Ausnahme verursachen:

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[0]; //check other elements in loop }

Dies führt zu einer NullPointerException in Zeile 6.

Der Zugriff auf ein Feld, eine Methode oder einen Index eines Nullobjekts führt daher zu einer NullPointerException , wie aus den obigen Beispielen hervorgeht.

Eine übliche Methode zum Vermeiden 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 identifizieren, welche Objekte null sein können. Eine aggressiv sichere Strategie könnte darin bestehen, für jedes Objekt Null zu prüfen . Dies führt jedoch zu vielen redundanten 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.

3. Behandlung von Null über den API-Vertrag

Wie im letzten Abschnitt erläutert, führt der Zugriff auf Methoden oder Variablen von Nullobjekten zu einer NullPointerException. Wir diskutierten auch , dass ein Putting null auf ein Objekt Prüfung , bevor es eliminiert die Möglichkeit des Zugriffs auf Nullpointer.

Oft 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 print () -Methodenaufruf würde nur "null" ausgeben, aber keine Ausnahme auslösen. In ähnlicher Weise würde process () in seiner Antwort niemals null zurückgeben . Es wird eher eine Ausnahme ausgelöst.

Für einen Clientcode, der auf die oben genannten APIs zugreift, ist keine Nullprüfung erforderlich.

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

Dies gibt jedoch keinen klaren Hinweis auf den API-Vertrag und ist daher auf die Client-Code-Entwickler angewiesen, um dessen Einhaltung sicherzustellen.

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

4. Automatisierung von API-Verträgen

4.1. Verwenden der statischen Code-Analyse

Statische Code-Analyse-Tools tragen dazu bei, die Codequalität erheblich zu verbessern. Einige dieser Tools ermöglichen es den Entwicklern auch, den Nullvertrag aufrechtzuerhalten . Ein Beispiel ist FindBugs.

FindBugs hilft , die Verwaltung von null Vertrag über die @Nullable und @NonNull Anmerkungen. Wir können diese Anmerkungen für jede Methode, jedes Feld, jede lokale Variable oder jeden Parameter verwenden. Dadurch wird für den Clientcode explizit angegeben, ob der mit Anmerkungen versehene 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 deutlich, dass das Argument nicht null sein kann. Wenn der Clientcode diese Methode aufruft, ohne das Argument auf null zu prüfen , generiert FindBugs beim Kompilieren eine Warnung.

4.2. Verwenden der IDE-Unterstützung

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

Einige IDEs ermöglichen Entwicklern auch die Verwaltung von API-Verträgen, wodurch die Notwendigkeit eines statischen Code-Analyse-Tools entfällt. IntelliJ IDEA bietet die Annotationen @NonNull und @Nullable . Um die Unterstützung für diese Anmerkungen in IntelliJ hinzuzufügen, müssen Sie die folgende Maven-Abhängigkeit hinzufügen:

 org.jetbrains annotations 16.0.2 

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

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

5. Behauptungen

Bisher haben wir nur darüber gesprochen, die Notwendigkeit von Nullprüfungen aus dem Clientcode zu entfernen . Dies ist jedoch in realen Anwendungen selten der Fall.

Nehmen wir nun an, wir arbeiten mit einer API, die keine Nullparameter akzeptieren oder eine Nullantwort zurückgeben kann, die vom Client verarbeitet werden muss . Dies bedeutet, dass wir die Parameter oder die Antwort auf einen Nullwert überprüfen müssen .

Here, we can use Java Assertions instead of the traditional null check conditional statement:

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

In line 2, we check for a null parameter. If the assertions are enabled, this would result in an AssertionError.

Although it is a good way of asserting pre-conditions like non-null parameters, this approach has two major problems:

  1. Assertions are usually disabled in a JVM
  2. A false assertion results in an unchecked error that is irrecoverable

Hence, it is not recommended for programmers to use Assertions for checking conditions. In the following sections, we'll discuss other ways of handling null validations.

6. Avoiding Null Checks Through Coding Practices

6.1. Preconditions

It's usually a good practice to write code that fails early. Therefore, if an API accepts multiple parameters that aren't allowed to be null, it's better to check for every non-null parameter as a precondition of the API.

For example, let's look at two methods – one that fails early, and one that doesn't:

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); } }

Clearly, we should prefer goodAccept() over badAccept().

As an alternative, we can also use Guava's Preconditions for validating API parameters.

6.2. Using Primitives Instead of Wrapper Classes

Since null is not an acceptable value for primitives like int, we should prefer them over their wrapper counterparts like Integer wherever possible.

Consider two implementations of a method that sums two integers:

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

Now, let's call these APIs in our client code:

int sum = primitiveSum(null, 2);

This would result in a compile-time error since null is not a valid value for an int.

And when using the API with wrapper classes, we get a NullPointerException:

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

There are also other factors for using primitives over wrappers, as we covered in another tutorial, Java Primitives versus Objects.

6.3. Empty Collections

Occasionally, we need to return a collection as a response from a method. For such methods, we should always try to return an empty collection instead of null:

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

Hence, we've avoided the need for our client to perform a null check when calling this method.

7. Using Objects

Java 7 introduced the new Objects API. This API has several static utility methods that take away a lot of redundant code. Let's look at one such method, requireNonNull():

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

Now, let's test the accept() method:

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

So, if null is passed as an argument, accept() throws a NullPointerException.

This class also has isNull() and nonNull() methods that can be used as predicates to check an object for null.

8. Using Optional

8.1. Using orElseThrow

Java 8 introduced a new Optional API in the language. This offers a better contract for handling optional values compared to null. Let's see how Optional takes away the need for null checks:

public Optional 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; } }

By returning an Optional, as shown above, the process method makes it clear to the caller that the response can be empty and must be handled at compile time.

This notably takes away the need for any null checks in the client code. An empty response can be handled differently using the declarative style of the Optional API:

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

Furthermore, it also provides a better contract to API developers to signify to the clients that an API can return an empty response.

Although we eliminated the need for a null check on the caller of this API, we used it to return an empty response. To avoid this, Optional provides an ofNullable method that returns an Optional with the specified value, or empty, if the value is null:

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

8.2. Using Optional with Collections

While dealing with empty collections, Optional comes in handy:

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

This function is supposed to return the first item of a list. The Stream API's findFirst function will return an empty Optional when there is no data. Here, we have used orElse to provide a default value instead.

This allows us to handle either empty lists, or lists, which after we have used the Stream library's filter method, have no items to supply.

Alternatively, we can also allow the client to decide how to handle empty by returning Optional from this method:

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

Therefore, if the result of getList is empty, this method will return an empty Optional to the client.

Using Optional with collections allows us to design APIs that are sure to return non-null values, thus avoiding explicit null checks on the client.

It's important to note here that this implementation relies on getList not returning null. However, as we discussed in the last section, it's often better to return an empty list rather than a null.

8.3. Combining Optionals

When we start making our functions return Optional we need a way to combine their results into a single value. Let's take our getList example from earlier. What if it were to return an Optional list, or were to be wrapped with a method that wrapped a null with Optional using ofNullable?

Our findFirst method wants to return an Optional first element of an Optional list:

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

By using the flatMap function on the Optional returned from getOptional we can unpack the result of an inner expression that returns Optional. Without flatMap, the result would be Optional . The flatMap operation is only performed when the Optional is not empty.

9. Libraries

9.1. Using Lombok

Lombok is a great library that reduces the amount of boilerplate code in our projects. It comes with a set of annotations that take the place of common parts of code we often write ourselves in Java applications, such as getters, setters, and toString(), to name a few.

Another of its annotations is @NonNull. So, if a project already uses Lombok to eliminate boilerplate code, @NonNull can replace the need for null checks.

Before we move on to see some examples, let's add a Maven dependency for Lombok:

 org.projectlombok lombok 1.18.6 

Now, we can use @NonNull wherever a null check is needed:

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

So, we simply annotated the object for which the null check would've been required, and Lombok generates the compiled class:

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

If param is null, this method throws a NullPointerException. The method must make this explicit in its contract, and the client code must handle the exception.

9.2. Using StringUtils

Generally, String validation includes a check for an empty value in addition to null value. Therefore, a common validation statement would be:

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

This quickly becomes redundant if we have to deal with a lot of String types. This is where StringUtils comes handy. Before we see this in action, let's add a Maven dependency for commons-lang3:

 org.apache.commons commons-lang3 3.8.1 

Let's now refactor the above code with StringUtils:

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

So, we replaced our null or empty check with a static utility method isNotEmpty(). This API offers other powerful utility methods for handling common String functions.

10. Schlussfolgerung

In diesem Artikel haben wir uns die verschiedenen Gründe für NullPointerException angesehen und erklärt, warum es schwierig ist, sie zu identifizieren. Dann haben wir verschiedene Möglichkeiten gesehen, um die Redundanz im Code beim Überprüfen auf Null mit Parametern, Rückgabetypen und anderen Variablen zu vermeiden .

Alle Beispiele sind auf GitHub verfügbar.