1. CompletableFuture kombinieren
In der Praxis kommt selten alles aus einer Quelle: Das Benutzerprofil und seine Bestellungen kann man parallel laden, Daten aus zwei Microservices – zusammenführen, und manchmal möchte man einfach die zuerst eingetroffene Antwort verarbeiten. Ein synchroner Ansatz zwingt hier zum sequentiellen Warten; die Klasse CompletableFuture erlaubt, alles gleichzeitig zu starten und die Ergebnisse elegant zu kombinieren. Dafür stellt sie spezielle Methoden bereit: thenCombine, allOf, anyOf. Schauen wir uns jede davon der Reihe nach an.
Parallele Aufgaben: zwei asynchrone Aufrufe
So sähe es synchron aus:
String name = loadUserName(); // dauert lange
int balance = loadUserBalance(); // dauert lange
System.out.println("Name: " + name + ", Kontostand: " + balance);
Problem: Der zweite Aufruf startet erst, nachdem der erste abgeschlossen ist.
Asynchrone Variante
Mit CompletableFuture lassen sich beide Aufgaben gleichzeitig starten:
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
Wie bekommt man nun beide Ergebnisse und verarbeitet sie gemeinsam? Dafür dient thenCombine.
2. thenCombine: Ergebnisse zweier Aufgaben zusammenführen
Die Methode thenCombine ermöglicht es, zwei CompletableFuture zu kombinieren und eine Aktion auszuführen, sobald beide Aufgaben abgeschlossen sind. Sie gibt ein neues CompletableFuture mit dem zusammengeführten Ergebnis zurück.
Signatur:
<A, B, C> CompletableFuture<C> thenCombine(
CompletionStage<? extends B> other,
BiFunction<? super A, ? super B, ? extends C> fn
)
- A – Ergebnistyp des ersten Future,
- B – Ergebnistyp des zweiten,
- C – Typ des zusammengeführten Ergebnisses.
Beispiel
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
CompletableFuture<String> resultFuture = nameFuture.thenCombine(
balanceFuture,
(name, balance) -> "Name: " + name + ", Kontostand: " + balance
);
resultFuture.thenAccept(System.out::println);
So funktioniert es:
- Beide Futures starten parallel.
- Sobald beide abgeschlossen sind, wird die Funktion (name, balance) -> ... aufgerufen.
- Das resultierende Future enthält den String mit dem Ergebnis.
Mini-Beispiel mit Zahlen
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> sum = f1.thenCombine(f2, Integer::sum);
sum.thenAccept(result -> System.out.println("Summe: " + result));
Ausgabe:
Summe: 5
Asynchrone Variante
Wenn das Zusammenführen rechenintensiv ist, verwenden Sie thenCombineAsync:
f1.thenCombineAsync(f2, (a, b) -> a * b);
3. allOf: wenn es viele Aufgaben gibt
Was ist, wenn wir nicht zwei Aufgaben, sondern gleich ein Dutzend haben? Zum Beispiel möchten wir parallel Daten für zehn Benutzer laden. Dafür gibt es die Methode CompletableFuture.allOf.
Beschreibung
CompletableFuture.allOf(f1, f2, ..., fn) gibt ein neues Future zurück, das abgeschlossen ist, wenn alle übergebenen Aufgaben abgeschlossen sind. Es gibt jedoch eine Besonderheit: Dieses Future enthält kein Ergebnis – sein Typ ist immer CompletableFuture<Void>. Um die Ergebnisse zu erhalten, muss man sie separat aus den ursprünglichen Futures „herausziehen“.
Beispiel
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Erster");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Zweiter");
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
all.thenRun(() -> {
// Alle Aufgaben sind abgeschlossen!
String s1 = f1.join(); // join() — wie get(), wirft aber eine unchecked Exception
String s2 = f2.join();
System.out.println(s1 + " & " + s2);
});
Ausgabe:
Erster & Zweiter
Beispiel mit einem Array von Aufgaben
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int id = i;
futures.add(CompletableFuture.supplyAsync(() -> "Benutzer " + id));
}
CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
all.thenRun(() -> {
for (CompletableFuture<String> f : futures) {
System.out.println(f.join());
}
});
Was passiert:
- Alle Aufgaben starten parallel.
- allOf ist abgeschlossen, wenn alle Aufgaben abgeschlossen sind.
- Im Block thenRun können wir die Ergebnisse über join() abrufen.
Visuelles Schema
[Future1] \
[Future2] ----> [allOf] ---> thenRun
[Future3] /
4. anyOf: auf die zuerst abgeschlossene Aufgabe warten
Manchmal muss man nicht auf alle warten, sondern das Ergebnis der schnellsten Aufgabe verwenden. Zum Beispiel fragen wir Daten bei zwei Servern an – wir verwenden die Daten desjenigen, der zuerst antwortet. Dafür gibt es CompletableFuture.anyOf.
Beschreibung
CompletableFuture.anyOf(f1, f2, ..., fn) gibt ein Future zurück, das abgeschlossen ist, sobald eine beliebige der übergebenen Aufgaben abgeschlossen ist. Der Ergebnistyp ist CompletableFuture<Object>, da die Typen der Aufgaben unterschiedlich sein können.
Beispiel
CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
sleep(500);
return "Schneller Server";
});
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "Langsamer Server";
});
CompletableFuture<Object> any = CompletableFuture.anyOf(fast, slow);
any.thenAccept(result -> System.out.println("Erhalten: " + result));
Ausgabe:
Erhalten: Schneller Server
Beispiel mit unterschiedlichen Typen
Man kann Aufgaben mit unterschiedlichen Typen kombinieren, aber dann ist das Ergebnis vom Typ Object, und eine explizite Typumwandlung ist erforderlich.
5. Nützliche Details
Empfehlungen
- allOf gibt kein Array von Ergebnissen zurück. Sie müssen die ursprünglichen Futures aufbewahren, um ihre Werte über join() oder get() zu erhalten.
- anyOf gibt das erste fertiggestellte Ergebnis zurück, aber der Typ ist Object. Wenn alle Aufgaben denselben Typ haben, können Sie einen Cast vornehmen.
- Wenn eine der Aufgaben in allOf mit einem Fehler endet, wird auch das resultierende Future mit einem Fehler abgeschlossen.
- Für thenCombine müssen beide Aufgaben erfolgreich abschließen, andernfalls gibt es eine Exception.
Vergleichstabelle der Methoden
| Methode | Wann verwenden | Ergebnistyp |
|---|---|---|
|
Wenn die Ergebnisse zweier Aufgaben zusammengeführt werden sollen | Zusammengeführtes Ergebnis |
|
Wenn auf den Abschluss aller Aufgaben gewartet werden soll | |
|
Wenn auf den Abschluss einer beliebigen Aufgabe gewartet werden soll | Object (Ergebnis der ersten Aufgabe) |
6. Häufige Fehler beim Kombinieren von CompletableFuture
Fehler Nr. 1: Auf das Ergebnis per get()/join() im Hauptthread warten.
Wenn Sie asynchronen Code schreiben, am Ende aber trotzdem get() oder join() aufrufen, blockieren Sie den Thread und verlieren die Vorteile der Asynchronität. Verwenden Sie besser thenAccept/thenRun für eine nicht blockierende Verarbeitung.
Fehler Nr. 2: Referenzen auf die ursprünglichen Futures bei der Verwendung von allOf nicht aufbewahren.
Wenn Sie CompletableFuture.allOf(f1, f2, f3) aufgerufen haben, f1, f2, f3 aber nicht gespeichert haben, können Sie ihre Ergebnisse nicht abrufen. allOf gibt nur Void zurück!
Fehler Nr. 3: Fehler in der Kette nicht behandeln.
Wenn eine der Aufgaben mit einem Fehler endet, wird das gesamte allOf oder thenCombine ebenfalls mit einem Fehler abgeschlossen. Verwenden Sie die Fehlerbehandlungsmethoden (exceptionally, handle, whenComplete), damit Exceptions nicht untergehen.
Fehler Nr. 4: Nicht übereinstimmende Typen bei anyOf.
anyOf gibt Object zurück. Wenn Ihre Futures unterschiedliche Typen liefern, müssen Sie prüfen, was zuerst eingetroffen ist. Verwenden Sie nach Möglichkeit gleiche Typen.
Fehler Nr. 5: Zu komplexe Ketten ohne Kommentare.
Wenn der Code umfangreich wird, können Future-Ketten zu „Spaghetti“ werden. Scheuen Sie sich nicht, die Kette in einzelne Variablen aufzuteilen und die Schritte zu kommentieren.
GO TO FULL VERSION