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) {}
}
}
GO TO FULL VERSION