Programmiersprache Java: Null-Fehler mit statischer Analyse aufspüren
NullPointerExceptions sind mit die häufigste Fehlerquelle in Java. Durch statische Codeanalyse kann man diese Fehler deutlich minimieren.
Neue Projekte bringen neue Herausforderungen und neues Wissen mit sich. In meinem aktuellen Projekt [1] habe ich vor Kurzem eine Definition zum Umgang von null-Checks bei der statischen Codeanalyse erstellt. Vielen im Projekt war es wichtig, dass Parameter nicht nur zur Laufzeit beispielsweise durch Objects.requireNonNull(…)
) überprüft werden, sondern auch direkt beim Kompilieren. Daher haben wir beschlossen, hier auch auf statischen Codeanalyse zur Überprüfung des Umgangs mit null
zu setzen.
Statische Codeanalyse
Bevor wir uns auf die verschieben Annotation und Checker Libraries stürzen, die es für Java gibt, möchte ich einmal kurz erläutern, worum es sich bei einer statischen Codeanalyse handelt. Hierbei wird der Programmcode beim Kompilieren durch ein Tooling überprüft. Das aktuell gängigste Tool in Java ist sicherlich SpotBugs, das sich in Builds mit Maven oder Gradle einbinden lässt und dessen Ergebnisse auch automatisiert auf Plattformen wie SonarCloud veröffentlicht werden können. Mit einer statische Codeanalyse kann man Probleme wie einen Speicherüberlauf, eine Endlosschleife oder "Out of Bound“-Fehler finden. Ein einfaches Beispiel ist eine Division durch 0. Sollte so etwas im Code vorkommen, kann die Analyse eine Warnung liefern oder je nach Konfiguration den kompletten Build fehlerhaft beenden. In unserem Projekt haben wir einen solchen Check in GitHub Actions, das die Ergebnisse direkt in SonarCloud [2] und einem Pull Request anzeigt.
Der korrekte Umgang mit null
Ein Problem beim Programmieren mit Java ist sicherlich der Umgang mit null
. Wobei ich persönlich klar die Meinung vertrete, dass null
seine Berechtigung hat, auch wenn Tony Hoare, der Erfindern der null-Referenz in der Programmierung, hierüber mittlerweile als ein „Billion-Dollar Mistake“ spricht
Allerdings kann man in Java nicht nativ definieren, ob ein Parameter null sein darf. Das hat man mittlerweile versucht, über verschiedene Mittel in der Klassenbibliothek zu lösen. Beispiele hierfür sind java.util.Optional
, Objects.requireNonNull(…)
oder auch JSR305 [3].
Ein anschauliches Beispiel für eine Programmiersprache, die eine native Unterstützung für null-Referenzen hat, ist Kotlin. Sie unterscheidet explizit zwischen nullable-Referenzen und nonnull-Referenzen. Hierbei sind Letztere der Standard, wobei einer Variablen mit einer solchen Referenz nie null zugewiesen werden kann. Benötigt man eine Variable, die null umfassen kann, muss man mit einer nullable-Referenz arbeiten. Diese wird über das ?
Zeichen angegeben. Der folgende Code beinhaltet ein Kotlin Beispiel für beide Referenzen:
var a: String = "abc" // Regular initialization means
// non-null by default
a = null // compilation error
var b: String? = "abc" // can be set to null
b = null // ok
Da es eine solche native Unterstützung in Java nicht gibt, versucht man sie über statische Codeanalyse so gut wie möglich zu integrieren. Generell werden hier zwei Annotationen benötigt, wobei eine (@Nullable
) definiert, dass ein Wert beziehungsweise eine Variable null sein kann, und die andere Annotation definiert, dass ein Wert oder eine Variable nie null sein darf (@NonNull
).
Zum Verständnis soll ein Code Beispiel dienen, das eine Methode definiert und per Annotation die Information hinzufügt, dass der Rückgabewert der Methode nie null
sein kann:
@NonNull String getName() {
if(isUnique()) {
return „Item „ + getId();
} else {
return null;
}
}
Wie man in der Implementierung der Methode sehen kann, ist es durchaus möglich, dass sie null
zurückgibt. Das wär ein Fall, in dem die statische Codeanalyse eine Verletzung aufzeigt. Wer möchte, kann beispielsweise IntelliJ so konfigurieren, dass es solche Probleme direkt anzeigt.
Der folgende Code, der die @Nullable
Annotation verwendet, führt zu einer Warnung in der Analyse:
void check(@Nullable String value) {
Objects.hash(value.toLowerCase());
}
In diesem Beispiel wird durch die Annotation @Nullable
für die Variable value
definiert, dass diese den Wert null
haben kann. Dass der Code allerdings direkt auf die Variable zugreift, führt potenziell zu einer NullPointerException
zur Laufzeit. Auch das würde durch die statische Codeanalyse ausgewertet und als Problem ausgegeben.
Der Einbau in das eigene Projekt
Wer eine solche statische Codeanalyse im eigenen Projekt integrieren möchte, muss ein paar einfache Voraussetzungen schaffen. Als Erstes muss man sich für eins oder mehrere Analysetools entscheiden. Hier empfehle ich Spotbugs [4], das der Nachfolger von Findbugs ist. Das Tool kann entweder über die Kommandozeile oder integriert in einen Gradle oder Maven Build gestartet werden. Um die gefundenen Probleme zu analysieren, kann man sich diese entweder im Spotbugs eigenen Swing-Client anschauen oder beispielsweise als HTML-basierte Übersicht als Bestandteil einer generierten Maven-Site mittels des Maven site
-Ziels. Man kann das Tool so konfigurieren, dass es die Ergebnisse beispielsweise in ein Sonar beziehungsweise die SonarCloud hochlädt.
Wer @Nullable
- und @NonNull
-Annotationen im Projekt nutzen möchte, benötigt eine Library, die die Annotation bereitstellt. Das eigene Projekt muss nur zur Compile-Zeit von der Library abhängig sein. Auch hier gibt es (leider) eine ganze Fülle an Bibliotheken, die Annotationen bereitstellen. Die einzelnen Libraries basierend auf ihren Vor- und Nachteilen zu beleuchten, wird Bestandteil eines eigenen Posts sein. Daher empfehle ich zunächst Spotbugs Annotations als Abhängigkeit, die man unter den folgenden Maven-Koordinaten finden kann:
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
<version>4.7.3</version>
</dependency>
Die Fülle an Tools und Libraries macht es einem bedauerlicherweise nicht leicht, die perfekte und zukunftsorientierte Kombination zu finden. Als ich tiefer in das Thema eingetaucht bin, war ich erschrocken, dass vieles in diesem Bereich noch immer nicht durch Standards oder allgemein genutzte Best Practices definiert ist. Zwar gab es hier verschiedene Ansätze wie etwa mit demJSR305 [5], aber diese sind immer irgendwann im Sande verlaufen und werden heute teils in einem wilden Mix genutzt. Deswegen werde ich auch diesem Problem in naher Zukunft einen eigenen Post widmen.
(rme [6])
URL dieses Artikels:
https://www.heise.de/-7351944
Links in diesem Artikel:
[1] https://github.com/hashgraph/hedera-services
[2] https://sonarcloud.io/project/overview?id=com.hedera.hashgraph%3Ahedera-services
[3] https://jcp.org/en/jsr/detail?id=305
[4] https://spotbugs.github.io
[5] https://jcp.org/en/jsr/detail?id=305
[6] mailto:rme@ix.de
Copyright © 2023 Heise Medien