Wenn du schon eine Weile mit Java arbeitest, bist du sicher über den Begriff Future gestolpert. Ein Future ist ein Platzhalter für ein Ergebnis, das erst in der Zukunft verfügbar ist - also das Ergebnis einer asynchronen Operation. Doch das klassische Future hat seine Grenzen: Du kannst es nicht elegant kombinieren, keine Callbacks anhängen und musst aktiv warten, bis das Ergebnis da ist. Genau hier kommt CompletableFuture ins Spiel.
Mit CompletableFuture lassen sich Aufgaben asynchron ausführen, verketten und kombinieren - ohne den Hauptthread zu blockieren. Das klingt zunächst komplex, ist aber mit ein paar Beispielen gut nachvollziehbar.
Ein einfaches Beispiel
Stell dir vor, du möchtest eine zeitintensive Berechnung starten - zum Beispiel das Laden von Daten aus einer Datenbank. Du möchtest aber, dass dein Programm währenddessen weiterläuft.
import java.util.concurrent.CompletableFuture;
public class Beispiel1 {
public static void main(String[] args) throws Exception {
System.out.println("Starte Programm...");
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000); // simuliert eine langsame Operation
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Daten geladen!";
});
System.out.println("Ich kann in der Zwischenzeit etwas anderes tun...");
String ergebnis = future.get(); // blockiert, bis das Ergebnis da ist
System.out.println(ergebnis);
}
}
Ausgabe:
Starte Programm...
Ich kann in der Zwischenzeit etwas anderes tun...
Daten geladen!
Hier siehst du, dass der Code innerhalb von supplyAsync in einem eigenen Thread ausgeführt wird. Der Aufruf von get() wartet, bis das Ergebnis fertig ist. Trotzdem kann dein Programm in der Zwischenzeit weiterlaufen.
Callbacks statt Warten
Das Warten mit get() blockiert deinen Thread - was oft unerwünscht ist. Besser ist es, eine Aktion auszuführen, sobald das Ergebnis da ist. Dafür gibt es thenAccept und thenApply.
import java.util.concurrent.CompletableFuture;
public class Beispiel2 {
public static void main(String[] args) throws Exception {
CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Download abgeschlossen";
}).thenAccept(result -> {
System.out.println("Callback: " + result);
});
System.out.println("Ich laufe weiter...");
Thread.sleep(2000); // warten, damit das Programm nicht zu frueh endet
}
}
Ausgabe:
Ich laufe weiter...
Callback: Download abgeschlossen
Jetzt siehst du: Kein get() mehr nötig! Sobald der asynchrone Task fertig ist, wird der Callback automatisch ausgeführt. Das Programm bleibt dabei reaktiv und flüssig.
Ergebnisse verketten
Ein großer Vorteil von CompletableFuture ist das einfache Verketten von Operationen. Du kannst also sagen: „Wenn das fertig ist, mach das - und dann das nächste.“
import java.util.concurrent.CompletableFuture;
public class Beispiel3 {
public static void main(String[] args) throws Exception {
CompletableFuture.supplyAsync(() -> {
System.out.println("Lade Benutzerdaten...");
sleep(1000);
return "Max Mustermann";
}).thenApply(name -> {
System.out.println("Verarbeite Daten...");
sleep(1000);
return "Benutzer: " + name.toUpperCase();
}).thenAccept(System.out::println);
sleep(2500); // Hauptthread kurz warten
}
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Ausgabe:
Lade Benutzerdaten...
Verarbeite Daten...
Benutzer: MAX MUSTERMANN
Mit thenApply kannst du das Ergebnis weiterverarbeiten und mit thenAccept eine finale Aktion ausführen. Jede Stufe läuft nacheinander, aber ohne dass du dich selbst um Threads kümmern musst.
Mehrere Futures kombinieren
Manchmal willst du mehrere Aufgaben gleichzeitig starten und danach etwas tun, wenn beide fertig sind. Das geht mit thenCombine oder allOf.
import java.util.concurrent.CompletableFuture;
public class Beispiel4 {
public static void main(String[] args) throws Exception {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "Task 1 fertig";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
sleep(1500);
return "Task 2 fertig";
});
CompletableFuture<String> combined = future1.thenCombine(future2, (a, b) -> a + " und " + b);
System.out.println(combined.get());
}
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Ausgabe:
Task 1 fertig und Task 2 fertig
Mit thenCombine wartest du, bis beide Futures fertig sind, und kombinierst deren Ergebnisse. Das ist perfekt, wenn du zum Beispiel Daten aus zwei Quellen laden musst.
Fehlerbehandlung
Wie immer gilt: Fehler passieren. Zum Glück hat CompletableFuture eingebaute Mechanismen dafür. Mit exceptionally kannst du Fehler elegant abfangen.
import java.util.concurrent.CompletableFuture;
public class Beispiel5 {
public static void main(String[] args) throws Exception {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("Etwas ist schiefgelaufen");
}
return "Alles gut";
}).exceptionally(ex -> {
System.out.println("Fehler: " + ex.getMessage());
return "Standardwert";
});
System.out.println(future.get());
}
}
Ausgabe:
Fehler: Etwas ist schiefgelaufen
Standardwert
Hier sorgt exceptionally dafür, dass dein Programm nicht abstürzt, sondern eine sinnvolle Reaktion zeigt. Gerade in produktiven Systemen ist das extrem wichtig.
Fazit
CompletableFuture ist ein mächtiges Werkzeug, das dir hilft, asynchrone Prozesse sauber, lesbar und effizient umzusetzen. Du kannst mehrere Aufgaben parallel starten, sie verketten, kombinieren und Fehler elegant behandeln - und das alles ohne komplexes Thread-Management.
Für Einsteiger ist es anfangs etwas ungewohnt, in „Asynchronität“ zu denken, aber mit kleinen Experimenten wirst du schnell merken, wie viel flexibler dein Code dadurch wird. Und das Beste: Du brauchst keine externen Bibliotheken - alles ist Teil von Java.
Wenn du dich also das nächste Mal dabei ertappst, wie du in einer Schleife auf ein Ergebnis wartest oder mit Thread.sleep() arbeitest, denk an CompletableFuture. Es ist der moderne Weg, Aufgaben in Java parallel und reaktiv zu gestalten.
