1. Future: 「準備ができるまで待つ」
Java の非同期操作は、手紙を郵送するのに似ています。ポストの前で返事を待つのではなく、自分の用事を進めておき、手紙が届いたら通知を受け取るだけです。コードでは、操作が完了したことを把握し、結果やエラーを知る必要があります。Java には Future と CompletionHandler の2つのアプローチがあります。
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 | いいえ | Truly async, 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("ファイル " + attachment + " に " + result + " バイトを正常に書き込みました");
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