CodeGym /課程 /JAVA 25 SELF /非同步任務:thenApply、thenAccept、thenRun

非同步任務:thenApply、thenAccept、thenRun

JAVA 25 SELF
等級 55 , 課堂 1
開放

1. 啟動非同步任務:supplyAsync 與 runAsync

啟動非同步任務最常見的方式是使用 CompletableFuture.supplyAsync。此方法接收會回傳結果的 lambda 或方法。例如,我們想要模擬從伺服器載入資料:

import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 模擬耗時操作(例如檔案下載)
            sleep(1000);
            return "來自伺服器的資料";
        });

        System.out.println("任務已啟動!");
        // ... 任務執行時,可以在這裡做其他事
    }

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

runAsync:當不需要結果時

如果你的任務不回傳任何值(例如只是寫入日誌、發送通知),請使用 runAsync

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    sleep(500);
    System.out.println("操作完成!");
});

runAsync 一律回傳 CompletableFuture<Void>,因為不期望有結果。

2. thenApply、thenAccept、thenRun:有什麼差別?

當非同步任務完成後,通常會想對結果做點什麼。為此提供了「處理器」方法:

  • thenApply — 轉換結果並回傳新結果。
  • thenAccept — 接收結果,但不回傳任何值(用於副作用)。
  • thenRun — 不接收結果,也不回傳任何值(僅在任務完成後執行一段動作)。

thenApply:處理並轉換結果

若需要 轉換 前一個任務的結果,就使用 thenApply。例如,下載到一個字串,現在想知道它的長度:

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

CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
    System.out.println("正在計算字串長度...");
    return s.length();
});

// 現在 lengthFuture 含有 Integer — 字串 "Java" 的長度
lengthFuture.thenAccept(len -> System.out.println("長度:" + len));

發生了什麼:

  • future 含有字串 "Java"
  • thenApply 將字串轉為其長度(int)。
  • thenAccept 輸出結果。

thenAccept:對結果執行動作(不回傳任何值)

如果只需要對結果做點事(例如印到螢幕),而不需要回傳任何值,請使用 thenAccept

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "你好,世界!");

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

thenAccept 就像「消費者」:它消耗結果並做一些有用的事。

thenRun:不需要結果的動作

如果你只想在任務完成後執行某個動作,但結果不需要,請使用 thenRun

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

future.thenRun(() -> {
    System.out.println("載入完成!");
});

請注意:在 thenRun 之中無法取得前一個任務的結果——它會被忽略。

3. 連鎖呼叫:把任務串成處理管線

CompletableFuture 的最大威力在於能建立計算的鏈結。每個方法(thenApplythenAcceptthenRun)都會回傳新的 CompletableFuture,你可以再對它加上處理器。

範例:多階段處理

讓我們完善一下應用:先載入資料、轉換它、輸出結果,最後寫入日誌表示一切完成。

CompletableFuture.supplyAsync(() -> {
    System.out.println("步驟 1:正在載入資料...");
    sleep(500);
    return "Java";
})
.thenApply(data -> {
    System.out.println("步驟 2:正在轉換資料...");
    return data.toUpperCase();
})
.thenAccept(result -> {
    System.out.println("步驟 3:輸出結果:" + result);
})
.thenRun(() -> {
    System.out.println("步驟 4:全部完成!");
});

主控台輸出:

步驟 1:正在載入資料...
步驟 2:正在轉換資料...
步驟 3:輸出結果:JAVA
步驟 4:全部完成!

請注意:
每個下一步都只會在前一步完成後才開始。這讓我們可以建立真正的資料處理「管線」。

4. 非同步變體:thenApplyAsync、thenAcceptAsync、thenRunAsync

預設情況下,處理器(thenApplythenAcceptthenRun)會在前一個任務完成的同一個執行緒中執行。有時這並不理想——若處理工作很耗資源,最好把它丟到另一個執行緒。

為此有對應的非同步版本:

  • thenApplyAsync
  • thenAcceptAsync
  • thenRunAsync

有什麼差別?

  • 沒有 Async: 處理器可能在與前一個任務相同的執行緒中執行(例如任務在 ForkJoinPool 完成,處理器也可能在那裡)。
  • 使用 Async: 處理器保證會在 ForkJoinPool(或你的 Executor)中的另一個執行緒執行。

範例:比較同步與非同步處理器

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("載入中... [" + 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";
});

典型輸出:

載入中... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]

結論:
非同步處理器會在不同的執行緒中執行。

何時使用 Async 方法?

  • 當處理很耗資源(例如繁重計算、網路存取)。
  • 當你不想阻塞前一個任務完成所在的執行緒。
  • 當你想明確管理執行緒(例如作為第二個參數傳入自訂的 Executor)。

5. 實用細節

表格:thenApply、thenAccept、thenRun 的比較

方法 會使用結果? 會回傳值? 適用情境
thenApply
轉換結果
thenAccept
副作用(輸出、記錄日誌)
thenRun
在任務完成後執行一段動作
thenApplyAsync
相同,但在另一個執行緒中
thenAcceptAsync
相同,但在另一個執行緒中
thenRunAsync
相同,但在另一個執行緒中

問題:長鏈要怎麼寫?

可以像拼 LEGO 一樣把方法一個接一個串起來:

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

輸出:

結果:84

每一步都會取得前一步的結果,可以修改它或僅僅使用它。

6. 使用 thenApply、thenAccept、thenRun 時的常見錯誤

錯誤 1:混淆回傳型別。
thenApply 必須回傳一個會傳遞到鏈中下一步的值。如果你誤用 thenApply 卻沒有回傳結果,下一個操作會拿到 null(或甚至無法編譯)。需要副作用時請使用 thenAcceptthenRun

錯誤 2:嘗試在 thenRun 內使用結果。
thenRun 內部無法存取前一個任務的結果。若要使用結果,請選擇 thenApplythenAccept

錯誤 3:阻塞主執行緒。
如果你在主執行緒呼叫 get()join(),就失去了非同步的所有優勢:執行緒會等待任務結束,就像傳統的同步程式碼一樣。最好使用不阻塞的鏈結與回呼。

錯誤 4:未處理例外。
如果在鏈結中發生例外,而你沒有加入處理器(exceptionallyhandlewhenComplete),它可能「悄悄」失敗,任務會以錯誤結束而你看不到。務必在鏈結中處理錯誤。

錯誤 5:意外地在其他執行緒中執行。
非同步方法(thenApplyAsync 等)可能在不同的執行緒執行。如果你存取未針對多執行緒保護的變數,可能會發生資料競爭。

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