CodeGym /Kurse /JAVA 25 SELF /Kombinieren von CompletableFuture: thenCombine, allOf, an...

Kombinieren von CompletableFuture: thenCombine, allOf, anyOf

JAVA 25 SELF
Level 55 , Lektion 2
Verfügbar

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
thenCombine
Wenn die Ergebnisse zweier Aufgaben zusammengeführt werden sollen Zusammengeführtes Ergebnis
allOf
Wenn auf den Abschluss aller Aufgaben gewartet werden soll
Void
anyOf
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.

1
Aufgabe
JAVA 25 SELF, Level 55, Lektion 2
Gesperrt
Koordination der Lieferungen: Warten auf alle Sendungen
Koordination der Lieferungen: Warten auf alle Sendungen
1
Aufgabe
JAVA 25 SELF, Level 55, Lektion 2
Gesperrt
Begrüßungsaufbau: Zusammenführen von Sprachfragmenten
Begrüßungsaufbau: Zusammenführen von Sprachfragmenten
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION