CodeGym /課程 /JAVA 25 SELF /Future、CompletionHandler:處理操作的完成

Future、CompletionHandler:處理操作的完成

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

1. Future:「等它準備好」

在 Java 中的非同步操作就像寄出一封郵件:你不會站在信箱前等待回覆,而是去忙自己的事。當信件到了——你只需收到通知。在程式碼裡,你需要知道操作何時完成,並取得結果或錯誤。在 Java 中有兩種做法:使用 Future,以及使用 CompletionHandler

使用 Future 時,你會拿到一張「收據」——對未來結果的承諾。你可以不時檢查它是否已就緒,或直接呼叫 get() 等待結果。

使用 CompletionHandler 就完全不用等:你預先指定處理器,當成功或失敗時它會自動被呼叫。

如何運作?

當你在 AsynchronousFileChannel 上呼叫非同步的 read()write() 方法時,可以取得一個 Future<Integer> 物件。這就像電子排隊的號碼牌:操作開始後,你可以隨時問一句:「好了沒?」。

方法簽名

Future<Integer> read(ByteBuffer dst, long position)

Future<Integer> write(ByteBuffer src, long position)
  • dst — 用來讀入資料的緩衝區。
  • src — 要寫出的資料來源緩衝區。
  • position — 檔案中進行讀/寫的位址位置。

範例:使用 Future 的非同步讀取

下列程式會非同步讀取檔案開頭的 1024 位元組,並顯式等待結果:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.io.IOException;

public class FutureReadExample {
    public static void main(String[] args) {
        Path path = Path.of("example.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            // 啟動非同步讀取
            Future<Integer> future = channel.read(buffer, 0);

            // ... 在檔案讀取期間可以先做其他事情

            // 等待作業完成(阻塞呼叫!)
            int bytesRead = future.get(); // 可能拋出 InterruptedException、ExecutionException

            System.out.println("讀取的位元組數:" + bytesRead);

            // 轉為從緩衝區讀出
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
        } catch (Exception e) {
            System.err.println("讀取檔案錯誤:" + e);
        }
    }
}
  • 方法 future.get() 會阻塞執行緒直到操作完成——就像「守在電話旁」等對方來電。
  • 可以先呼叫 future.isDone() 來確認操作是否完成,然後再呼叫 get()
  • 若操作以錯誤結束,get() 會拋出 ExecutionException(內含原因)。

什麼時候適合用 Future?

當你想啟動工作,並在合適的時候再等待結果時,Future 很好用:例如讀檔、發出請求、在背景計算。它也適合一次啟動多個操作,之後再彙整結果。

但如果你需要真正的非同步、避免阻塞,並且希望在完成時自動通知,那就應該考慮 CompletionHandler

2. CompletionHandler:「結束了再叫我」

CompletionHandler 是你實作並傳入 read()write() 方法的介面。當操作完成(或發生錯誤)時,Java 會自動呼叫你的處理器。這就像留下電話號碼:「準備好了請打給我」。

方法簽名

void read(ByteBuffer dst,
          long position,
          A attachment,
          CompletionHandler<Integer, ? super A> handler)
  • dst — 讀取用的緩衝區。
  • position — 檔案中的位置。
  • attachment — 會傳入處理器的任意物件(可以是 null,也可以是檔名等)。
  • handler — 你的處理器。

CompletionHandler 介面

public interface CompletionHandler<V, A> {
    void completed(V result, A attachment);
    void failed(Throwable exc, A attachment);
}
  • completed 在成功時呼叫;result 是位元組數,attachment 是你的物件。
  • failed 在發生錯誤時呼叫;exc 是例外。

範例:使用 CompletionHandler 的非同步讀取

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.IOException;

public class CompletionHandlerReadExample {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("example.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        channel.read(buffer, 0, "example.txt", new CompletionHandler<Integer, String>() {
            @Override
            public void completed(Integer result, String attachment) {
                System.out.println("檔案 " + attachment + " 已讀取,位元組數:" + result);
                buffer.flip();
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get());
                }
                closeChannel();
            }

            @Override
            public void failed(Throwable exc, String attachment) {
                System.err.println("讀取檔案 " + attachment + " 時發生錯誤:" + exc);
                closeChannel();
            }

            private void closeChannel() {
                try {
                    channel.close();
                } catch (IOException e) {
                    System.err.println("關閉通道時發生錯誤:" + e);
                }
            }
        });

        // 重要:如果不「暫留」執行緒,main 方法可能會在讀取完成前就結束!
        // 在真實應用中執行緒通常不會立刻結束(例如伺服器、UI)。
        try {
            Thread.sleep(500); // 給非同步作業一點時間(僅示範用!)
        } catch (InterruptedException ignored) {}
    }
}

我們把匿名處理器傳給 read(),它知道在完成後該做什麼。在 completed 取得結果並處理,在 failed 中對錯誤做出反應。別忘了在處理器內關閉通道,以免留下未釋放的資源。

有個小地方要注意:main 方法可能會早於非同步操作完成前就結束,因此範例中加了 Thread.sleep(500)——只是為了看到結果。在真實應用中通常不需要這種技巧。

什麼時候更適合使用 CompletionHandler?

當你需要真正的非同步、不要等待與阻塞時,CompletionHandler 很合適:你啟動操作,並描述完成後要做的事。這對 UI(JavaFX、Swing)很關鍵,避免介面「卡住」,也對伺服器端很有用——執行緒不會空等,只有在有工作時才被使用。

4. 比較 Future 與 CompletionHandler

做法 會阻塞執行緒? 何時使用? 範例情境
Future 會(在 get() 時) 簡單的順序處理 檔案複製、報表
CompletionHandler 不會 真正的非同步、UI、伺服器、平行作業 伺服器、GUI、大量 I/O
  • Future——較易理解,但取得結果需要阻塞。
  • CompletionHandler——稍複雜一些,但能提供真正的非同期性且不會阻塞執行緒。

5. 實作:使用 CompletionHandler 進行非同步寫入檔案

以下範例會非同步將字串寫入檔案,並回報結果:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class CompletionHandlerWriteExample {
    public static void main(String[] args) throws IOException {
        String text = "嗨,非同步世界!";
        ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
        Path path = Path.of("async_output.txt");

        AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        channel.write(buffer, 0, path, new CompletionHandler<Integer, Path>() {
            @Override
            public void completed(Integer result, Path attachment) {
                System.out.println("已成功寫入 " + result + " 位元組到檔案 " + attachment);
                try {
                    channel.close();
                } catch (IOException e) {
                    System.err.println("關閉通道時發生錯誤:" + e);
                }
            }

            @Override
            public void failed(Throwable exc, Path attachment) {
                System.err.println("寫入檔案 " + attachment + " 時發生錯誤:" + exc);
                try {
                    channel.close();
                } catch (IOException e) {
                    System.err.println("關閉通道時發生錯誤:" + e);
                }
            }
        });

        // 暫留 main 以便看到結果(僅示範用)
        try {
            Thread.sleep(500);
        } catch (InterruptedException ignored) {}
    }
}
1
任務
JAVA 25 SELF, 等級 56, 課堂 1
上鎖
非同步檔案開頭檢查與通知 🕵️‍♀️
非同步檔案開頭檢查與通知 🕵️‍♀️
1
任務
JAVA 25 SELF, 等級 56, 課堂 1
上鎖
帶有元資料的非同步遙測資料寫入 🛰️
帶有元資料的非同步遙測資料寫入 🛰️
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION