CodeGym /Kurse /JAVA 25 SELF /Asynchrone Aufgaben: thenApply, thenAccept, thenRun

Asynchrone Aufgaben: thenApply, thenAccept, thenRun

JAVA 25 SELF
Level 55 , Lektion 1
Verfügbar

1. Start einer asynchronen Aufgabe: supplyAsync und runAsync

Die gebräuchlichste Methode, eine asynchrone Aufgabe zu starten, ist CompletableFuture.supplyAsync. Diese Methode nimmt eine Lambda oder eine Methode entgegen, die ein Ergebnis zurückgibt. Beispiel: Wir wollen das Laden von Daten vom Server simulieren:

import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // Simulation einer langwierigen Operation (z. B. Datei herunterladen)
            sleep(1000);
            return "Daten vom Server";
        });

        System.out.println("Aufgabe gestartet!");
        // ... hier kann man etwas anderes tun, während die Aufgabe läuft
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

runAsync: wenn kein Ergebnis benötigt wird

Wenn Ihre Aufgabe nichts zurückgibt (z. B. nur ins Log schreibt oder eine Benachrichtigung sendet), verwenden Sie runAsync:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    sleep(500);
    System.out.println("Vorgang abgeschlossen!");
});

runAsync gibt immer CompletableFuture<Void> zurück, weil kein Ergebnis vorgesehen ist.

2. thenApply, thenAccept, thenRun: worin liegt der Unterschied?

Wenn eine asynchrone Aufgabe beendet ist, möchte man üblicherweise etwas mit dem Ergebnis tun. Dafür gibt es die „Handler“-Methoden:

  • thenApply – transformiert das Ergebnis und gibt ein neues Ergebnis zurück.
  • thenAccept – nimmt das Ergebnis entgegen, gibt nichts zurück (für Nebeneffekte gedacht).
  • thenRun – nimmt kein Ergebnis entgegen und gibt nichts zurück (führt einfach eine Aktion nach Abschluss der Aufgabe aus).

thenApply: Verarbeitung und Transformation des Ergebnisses

Wenn Sie das Ergebnis der vorherigen Aufgabe transformieren möchten, verwenden Sie thenApply. Beispiel: Wir haben einen String geladen und wollen nun seine Länge wissen:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");

CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
    System.out.println("Länge des Strings wird berechnet...");
    return s.length();
});

// lengthFuture enthält jetzt ein Integer - die Länge des Strings "Java"
lengthFuture.thenAccept(len -> System.out.println("Länge: " + len));

Was passiert:

  • future enthält den String "Java".
  • thenApply transformiert den String in seine Länge (int).
  • thenAccept gibt das Ergebnis aus.

thenAccept: Aktion mit dem Ergebnis (gibt nichts zurück)

Wenn Sie einfach etwas mit dem Ergebnis machen möchten (z. B. es ausgeben), ohne etwas zurückzugeben – verwenden Sie thenAccept:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hallo, Welt!");

future.thenAccept(result -> {
    System.out.println("Ergebnis: " + result);
});

thenAccept ist wie ein „Consumer“: Er verbraucht das Ergebnis und tut etwas Nützliches damit.

thenRun: Aktion ohne Ergebnis

Wenn Sie nach Abschluss der Aufgabe einfach eine Aktion ausführen möchten, das Ergebnis aber nicht benötigen, verwenden Sie thenRun:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Fertig!");

future.thenRun(() -> {
    System.out.println("Laden abgeschlossen!");
});

Beachten Sie: In thenRun können Sie das Ergebnis der vorherigen Aufgabe nicht erhalten – es wird schlicht ignoriert.

3. Aufrufketten: wir bauen eine Pipeline aus Aufgaben

Die größte Stärke von CompletableFuture ist der Aufbau von Berechnungsketten. Jede Methode (thenApply, thenAccept, thenRun) gibt ein neues CompletableFuture zurück, an das man erneut einen Handler anhängen kann.

Beispiel: mehrstufige Verarbeitung

Erweitern wir unsere Anwendung: Daten laden, sie transformieren, das Ergebnis ausgeben und ins Log schreiben, dass alles beendet ist.

CompletableFuture.supplyAsync(() -> {
    System.out.println("Schritt 1: Daten werden geladen...");
    sleep(500);
    return "Java";
})
.thenApply(data -> {
    System.out.println("Schritt 2: Daten werden transformiert...");
    return data.toUpperCase();
})
.thenAccept(result -> {
    System.out.println("Schritt 3: Ergebnis ausgeben: " + result);
})
.thenRun(() -> {
    System.out.println("Schritt 4: Alles abgeschlossen!");
});

Konsolenausgabe:

Schritt 1: Daten werden geladen...
Schritt 2: Daten werden transformiert...
Schritt 3: Ergebnis ausgeben: JAVA
Schritt 4: Alles abgeschlossen!

Beachten Sie:
Jeder nächste Schritt beginnt erst nach Abschluss des vorherigen. So lassen sich echte „Pipelines“ zur Datenverarbeitung bauen.

4. Asynchrone Varianten: thenApplyAsync, thenAcceptAsync, thenRunAsync

Standardmäßig werden die Handler (thenApply, thenAccept, thenRun) in dem Thread ausgeführt, in dem die vorherige Aufgabe beendet wurde. Das ist nicht immer bequem – ist die Verarbeitung schwergewichtig, bringt man sie besser in einen separaten Thread.

Dafür gibt es die asynchronen Varianten:

  • thenApplyAsync
  • thenAcceptAsync
  • thenRunAsync

Worin liegt der Unterschied?

  • Ohne Async: Der Handler kann im selben Thread ausgeführt werden wie die vorherige Aufgabe (z. B. wenn die Aufgabe im ForkJoinPool beendet wurde, dann auch der Handler dort).
  • Mit Async: Der Handler wird garantiert in einem anderen Thread aus dem ForkJoinPool (oder Ihrem Executor) ausgeführt.

Beispiel: normalen und asynchronen Handler vergleichen

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("Laden... [" + Thread.currentThread().getName() + "]");
    return "Hello";
});

future.thenApply(result -> {
    System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
    return result + " World";
});

future.thenApplyAsync(result -> {
    System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
    return result + " Async World";
});

Typische Ausgabe:

Laden... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]

Fazit:
Der asynchrone Handler wird in einem anderen Thread ausgeführt.

Wann sollte man die Async-Methoden verwenden?

  • Wenn die Verarbeitung ressourcenintensiv ist (z. B. aufwendige Berechnungen, Netzwerkarbeit).
  • Wenn Sie den Thread nicht blockieren möchten, in dem die vorherige Aufgabe beendet wurde.
  • Wenn Sie die Threads explizit steuern möchten (z. B. einen eigenen Executor als zweites Argument übergeben).

5. Nützliche Details

Tabelle: Vergleich der Methoden thenApply, thenAccept, thenRun

Methode Verwendet das Ergebnis? Gibt einen Wert zurück? Wofür verwenden
thenApply
Ja Ja Transformation des Ergebnisses
thenAccept
Ja Nein Nebeneffekte (Ausgabe, Logging)
thenRun
Nein Nein Nur eine Aktion nach Abschluss der Aufgabe
thenApplyAsync
Ja Ja Dasselbe, aber in einem anderen Thread
thenAcceptAsync
Ja Nein Dasselbe, aber in einem anderen Thread
thenRunAsync
Nein Nein Dasselbe, aber in einem anderen Thread

Frage: Wie baut man lange Ketten?

Man kann die Methoden nacheinander aufrufen – wie mit LEGO‑Bausteinen:

CompletableFuture.supplyAsync(() -> "42")
    .thenApply(Integer::parseInt)
    .thenApply(x -> x * 2)
    .thenAccept(x -> System.out.println("Ergebnis: " + x));

Ausgabe:

Ergebnis: 84

Jeder nächste Schritt erhält das Ergebnis des vorherigen, kann es ändern oder einfach verwenden.

6. Typische Fehler im Umgang mit thenApply, thenAccept, thenRun

Fehler Nr. 1: Verwechslung der Rückgabetypen.
thenApply muss einen Wert zurückgeben, der in der Kette weitergegeben wird. Wenn Sie versehentlich thenApply verwenden, aber kein Ergebnis zurückgeben, erhält die nächste Operation null (oder es kompiliert gar nicht). Für Nebeneffekte verwenden Sie thenAccept oder thenRun.

Fehler Nr. 2: Versuch, das Ergebnis in thenRun zu verwenden.
In thenRun gibt es keinen Zugriff auf das Ergebnis der vorherigen Aufgabe. Wenn Sie das Ergebnis verwenden möchten, wählen Sie thenApply oder thenAccept.

Fehler Nr. 3: Blockieren des Haupt‑Threads.
Wenn Sie get() oder join() im Haupt‑Thread aufrufen, verlieren Sie die Vorteile der Asynchronität: Der Thread wartet auf den Abschluss der Aufgabe – wie im guten alten synchronen Code. Besser sind nicht blockierende Ketten und Callbacks.

Fehler Nr. 4: Fehlende Fehlerbehandlung.
Wenn in der Kette eine Exception auftritt und Sie keinen Handler hinzugefügt haben (exceptionally, handle, whenComplete), „geht sie verloren“ und die Aufgabe kann mit einem Fehler enden, den Sie nicht sehen. Behandeln Sie in Ketten immer Fehler.

Fehler Nr. 5: Unerwartete Ausführung in einem anderen Thread.
Asynchrone Methoden (thenApplyAsync u. a.) können in einem anderen Thread ausgeführt werden. Wenn Sie auf Variablen zugreifen, die nicht gegen nebenläufigen Zugriff geschützt sind, kann es zu Race Conditions kommen.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION