NullPointerException — der ungeliebte Dauergast jeder Java-Codebase. Tony Hoare nannte seine Erfindung der Null-Referenz einmal seinen „Milliarden-Dollar-Fehler“. Moderne Sprachen wie Kotlin und Swift lösen das mit nicht-nullbaren Typen — Java selbst aber nicht.
Zwei Werkzeuge ändern das: JSpecify 1.0 als standardisierte Annotationen und NullAway als Compile-Time-Checker von Uber. Dieser Artikel zeigt, wie du beide kombinierst, um NPEs zu elimieren, bevor sie jemals in Produktion laufen.
Das Problem: Fragmentierte @Nullable-Landschaft
Jeder definiert @Nullable selbst: javax.annotation, org.jetbrains.annotations, org.checkerframework.checker.nullness.qual, edu.umd.cs.findbugs.annotations, … Keines dieser Annotations ist standardisiert, und keines verhält sich tool-übergreifend konsistent.
JSpecify 1.0.0 (Juli 2024) löst das: Ein Konsortium aus Google, Uber, JetBrains und Oracle hat sich auf vier Nullness-Annotationen mit präziser Semantik geeinigt.
JSpecify: Die vier Annotationen
import org.jspecify.annotations.*;
@NullMarked <em>// Alle nicht-annotierten Typen in diesem Scope sind @NonNull</em>
public class BenutzerService {
<em>// @Nullable auf Typs — der Rückgabetyp kann null sein</em>
public @Nullable Benutzer findeNachEmail(String email) {
return benutzerDb.get(email); <em>// darf null zurückgeben</em>
}
<em>// Ohne @Nullable: @NonNull implizit — der Parameter darf nicht null sein</em>
public void aktualisiereEmail(Benutzer benutzer, String neueEmail) {
benutzer.setEmail(neueEmail); <em>// NPE-safe: benutzer ist garantiert nicht null</em>
}
}
Code-Sprache: JavaScript (javascript)
| Annotation | Zweck |
|---|---|
@Nullable | Type-Use: Dieser Typ kann null sein |
@NullMarked | Scope: Alles in diesem Bereich ist default @NonNull |
@NullUnmarked | Scope: Hebt @NullMarked für einen Teilbereich auf (inkrementelle Migration) |
@NonNull | Type-Use: Explizit nicht null (meist redundant, da Default) |
Wichtig: @NullMarked gilt für Parameter, Rückgabewerte und Felder — nicht für lokale Variablen (diese sind durch Flow-Typisierung automatisch erfasst).
NullAway: Der Compile-Time-Enforcer
NullAway (aktuell 0.13.7) ist ein Error-Prone-Plugin, das Null-Verletzungen zur Compile-Zeit erkennt — ohne Laufzeit-Overhead. Anders als der Checker Framework (vollständig, aber langsam) verfolgt NullAway einen pragmatischen Ansatz: Es erkennt ~95% aller NPEs mit weniger als 10% Build-Zeit-Overhead.
Setup mit Gradle (empfohlen)
plugins {
id 'java'
id 'net.ltgt.errorprone' version '5.1.0'
}
dependencies {
errorprone 'com.uber.nullaway:nullaway:0.13.7'
errorprone 'com.google.errorprone:error_prone_core:2.50.0'
api 'org.jspecify:jspecify:1.0.0'
}
tasks.withType(JavaCompile).configureEach {
options.errorprone {
check('NullAway', CheckSeverity.ERROR)
option('NullAway:AnnotatedPackages', 'com.meinefirma')
option('NullAway:JSpecifyMode', 'true')
}
if (name.toLowerCase().contains('test')) {
options.errorprone.disable('NullAway')
}
}
Code-Sprache: JavaScript (javascript)
Setup mit Maven
<dependencies>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<compilerArgs>
<arg>-Xplugin:ErrorProne
-Xep:NullAway:ERROR
-XepOpt:NullAway:AnnotatedPackages=com.meinefirma
-XepOpt:NullAway:JSpecifyMode=true</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.50.0</version>
</path>
<path>
<groupId>com.uber.nullaway</groupId>
<artifactId>nullaway</artifactId>
<version>0.13.7</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Code-Sprache: HTML, XML (xml)
Was NullAway erkennt
NullAway führt eine intra-prozedurale Datenflussanalyse durch und findet diese typischen Fehler:
Bug 1: null übergeben, wo @NonNull erwartet wird
@NullMarked
public class Validierung {
static String sichereLaenge(String s) {
return s.length() + " Zeichen"; <em>// NPE-safe: s ist @NonNull</em>
}
static void test() {
sichereLaenge(null); <em>// ❌ COMPILE-ERROR: @Nullable an @NonNull übergeben</em>
}
}
Code-Sprache: JavaScript (javascript)
Bug 2: @Nullable dereferenzieren ohne Null-Check
@NullMarked
public class Validierung {
static void log(@Nullable Object x) {
System.out.println(x.toString()); <em>// ❌ COMPILE-ERROR: x ist @Nullable</em>
}
static void logFixed(@Nullable Object x) {
if (x != null) {
System.out.println(x.toString()); <em>// ✅ OK: Datenfluss erkennt x ≠ null</em>
}
}
}
Code-Sprache: JavaScript (javascript)
Bug 3: @NonNull-Feld nicht initialisiert
@NullMarked
public class Person {
String name; <em>// @NonNull implizit</em>
Person() {} <em>// ❌ COMPILE-ERROR: Field 'name' nicht initialisiert</em>
}
Code-Sprache: JavaScript (javascript)
Bug 4: @Nullable aus @NonNull-Methode zurückgeben
@NullMarked
public class Factory {
@Nullable String vielleichtNull() { return null; }
String garantiertNichtNull() {
return vielleichtNull(); <em>// ❌ COMPILE-ERROR</em>
}
}
Code-Sprache: JavaScript (javascript)
Inkrementelle Migration mit @NullUnmarked
Bestehende Codebasen migriert man schrittweise:
@NullUnmarked <em>// Altes Package: Keine Prüfungen</em>
package com.meinefirma.legacy;
<em>// ---</em>
@NullMarked <em>// Neues Package: Volle Prüfungen</em>
package com.meinefirma.buchhaltung;
Code-Sprache: HTML, XML (xml)
NullAway’s Option NullAway:OnlyNullMarked prüft automatisch nur @NullMarked-Scopes.
JSpecify und Kotlin
Ein großer Vorteil von JSpecify: Kotlin liest die Annotationen direkt. Wenn du eine Java-Bibliothek mit @NullMarked annotierst, erkennt der Kotlin-Compiler deren Nullness-Signaturen:
<em>// Java-Klasse mit JSpecify:</em>
<em>// @NullMarked</em>
<em>// public class Calculator {</em>
<em>// public @Nullable String format(int n) { ... }</em>
<em>// }</em>
<em>// Kotlin-Consumer erkennt automatisch:</em>
val result: String? = Calculator().format(42) <em>// ? nullable</em>
<em>// Ohne JSpecify wäre es String! (platform type) → keine Prüfung</em>
Code-Sprache: HTML, XML (xml)
Vergleich der Tools
| NullAway + JSpecify | Checker Framework | IntelliJ Inspections | |
|---|---|---|---|
| Standardisiert | Ja (JSpecify 1.0) | Nein | Nein |
| Build-Zeit-Overhead | <10% | Erheblich | Keiner (nur IDE) |
| Erkennt | ~95% | 100% (sound) | ~80% (Heuristiken) |
| Kotlin-Interop | Ja | Nein | Teilweise |
| Inkrementell | Ja (@NullUnmarked) | Ja (@SuppressWarnings) | Immer aktiv |
Fazit
JSpecify und NullAway zusammen geben Java das, was Kotlin und Swift von Haus aus haben: Compile-Time-Null-Safety. Der Setup-Aufwand ist minimal (eine Dependency, eine Annotation), der Gewinn immens: Keine NullPointerException in annotiertem Code.
Beginne mit neuen Packages unter @NullMarked und migriere bestehenden Code schrittweise. In einem Jahr wirst du dich fragen, wie du jemals ohne leben konntest.