Java: Top 3 Anti-Patterns für NullPointerExceptions
Die NullPointerException
gehört gefühlt zu den ärgerlichsten Laufzeitproblemen von Java-Programmen. Viele Entwickler versuchen das Problem durch zusätzliche Sonderbehandlungen für null
in den Griff zu bekommen – und übersehen, dass dadurch lediglich die Symptome bekämpft werden. Denn die Hauptursache liegt nicht bei der fehlenden Behandlung sondern bei der übermäßigen Nutzung von null
.
Es lassen sich einige wiederkehrende Muster falscher Verwendung erkennen – sogenannte Anti-Patterns, durch deren Vermeidung der Wert des Quelltexts im Allgemeinen zunimmt. Die aus meiner Sicht wichtigsten Anti-Patterns bezüglich NullPointerExceptions
beschreibe ich in den folgenden Abschnitten.
Anti-Pattern: Optimierung mit null
Erstaunlich häufig sieht man, dass null
als alternative Darstellung für leere Zeichenketten, leere Arrays, leere Listen oder Ähnliches verwendet wird. Durch diese Optimierung sollen unnötige Erzeugungen von Objekten vermieden und dadurch das Laufzeitverhalten verbessert werden.
Dabei werden jedoch zwei wichtige Punkte immer wieder vernachlässigt: Erstens verursacht diese Optimierung zusätzlichen und unnötigen Aufwand an anderen Stellen, wie in den beiden folgenden Code-Fragmenten zu sehen. Und zweitens gibt es für die meisten Situationen alternative und besser geeignete Optimierungen, sofern sie überhaupt sinnvoll sind. So erhält man beispielsweise mit Collections.emptyList()
eine leere und unveränderbare List
– und zwar ganz ohne Objektkonstruktion.
// with null optimization
List<Comment> comments = article.getComments();
if (comments != null) {
for (Comment comment : comments) {
// process comment
}
}
// without null optimization
for (Comment comment : article.getComments()) {
// process comment
}
Im Allgemeinen gilt: Man sollte null
nicht als alternative Darstellung für Objekte verwenden, wenn Objekte die fachliche Sicht besser repräsentieren.
Anti-Pattern: Fehlerbehandlung mit null
Das folgende Beispiel ist exemplarisch für die falsche Verwendung von null
im Zusammenhang mit Fehlerbehandlung. Die gezeigte Methode liefert zu einer Kundennummer das zugehörige Kunden-Objekt. Existiert kein Kunde mit dieser Kundennummer, so wird stattdessen null
zurückgegeben. Doch wo liegt das Problem bei diesem Code?
public Customer fetchCustomerById(int customerId) {
for (Customer customer : this.customers) {
if (customer.getCustomerId() == customerId) {
return customer;
}
}
return null; // customer not found
}
Man kann beliebig lange darüber diskutieren, ob eine nicht gefundene Kundennummer einen Fehler darstellt oder nicht – alleine schon aufgrund der unterschiedlichen Interpretationen des Begriffs Fehler
. Für die Fehlerbehandlung ist dagegen entscheidend, was der Aufrufer erwartet. In diesem Fall ist die Situation relativ klar: Da der Aufrufer bereits eine Kundennummer kennt, wird er wohl erwarten, dass auch ein Kunde mit dieser Kundennummer existiert. Alles andere sind Ausnahmen
und sollte mit Ausnahmen behandelt werden.
Im Allgemeinen gilt: Eine Methode sollte nicht null
zurückliefern, wenn der Aufrufer aufgrund der fachlichen Definition der Methode diesen Fall nicht erwartet. Stattdessen sollte eine Ausnahme geworfen werfen, die der Aufrufer im Zweifelsfall explizit behandeln kann.
Anti-Pattern: Weitergabe von null
Das folgende Listing zeigt im Ansatz eine Methode, die eine Zeichenkette an den Vorkommen des angegebenen Trennzeichens zerlegt und die einzelnen Fragmente zurückgibt. Man stelle sich vor, dass man in irgendeiner Situation während der Ausführung eine NullPointerException
erhält, die in der dritten Zeile bei dem Ausdruck value.indexOf(separator)
geworfen wird. Was macht man also? Man erweitert die Methode um eine if
-Bedingung, die den Fall value == null
abfängt und null
zurückgibt. Richtig?
public List<String> split(String value, char separator) {
List<String> result = new ArrayList<>();
int index = value.indexOf(separator);
// ...
return result;
}
Nein, natürlich nicht. Denn diese Änderung würde das eigentliche Problem überhaupt nicht lösen. Dennoch habe ich bereits überraschend viele Implementierungen gesehen, die genau das machen: Die Methoden enthalten Sonderbehandlungen für null
, die nicht zur eigentlichen Funktionalität gehören. Dadurch wird das eigentliche Problem nur verschleppt, mit der Konsequenz, dass die NullPointerExceptions
schließlich an Stellen auftreten, von denen man kaum noch auf die tatsächliche Ursache schließen kann.
Im Allgemeinen gilt: Die Fehlerbehebung wird signifikant vereinfacht, wenn die Fehlersymptome in engem Zusammenhang zu den Fehlerursachen erfasst werden. null
-Referenzen sollten weder toleriert noch weitergegeben werden, denn sie führen in der Regel nur dazu, dass null
-Referenzen noch an vielen Stellen auftreten, wo man sie überhaupt nicht erwarten würde..