Future を一言でいうと
java.util.concurrent.Future は、
「別スレッドで走っている“将来の計算結果”を受け取るための“約束の箱”」
です。
時間のかかる処理を ExecutorService に投げると、
その場で結果は返ってきません。代わりに Future が返されます。
その Future に対して、
- 終わったかどうか確認する
- 終わるまで待って結果をもらう
- タイムアウトを決めて待つ
- もういらないからキャンセルする
といった操作ができます。
「非同期処理のハンドル」としての役割を持っているのが Future です。
まずは全体像:Future が関わる典型パターン
ExecutorService との関係
Future は、単体で new することは基本的にありません。
普通は ExecutorService の submit メソッドを使うときに、一緒に出てきます。
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
// 時間のかかる処理
Thread.sleep(2000);
return 42;
});
Javaここで起きていることは、
submitした瞬間、別スレッドで処理が開始される- その結果(ここでは
Integer)を、後で受け取るためのFuture<Integer>が返ってくる
ということです。
すぐに結果は返ってこない。
でも「そのうち終わるはず」という“約束”だけは手元にある。
その約束を表現しているのが Future です。
一番大事なメソッド:get() で結果を受け取る
get() は「終わるまで待つ」メソッド
Future の本命メソッドは get() です。
Integer result = future.get();
System.out.println("結果 = " + result);
Javaget() は、
- タスクがまだ終わっていなければ、「終わるまでブロック(待機)」する
- 終わっていれば、即座に結果を返す
という挙動をします。
つまり、
「とりあえず非同期に投げておいて、必要になったタイミングで結果を取りに行く」
という書き方ができます。
get() が投げる 2 種類の例外
get() はチェック例外を 2 つ投げる可能性があります。
InterruptedException
待っている間に現在のスレッドが割り込まれたとき
ExecutionException
タスクの中で例外が発生したとき
例を見てみましょう。
import java.util.concurrent.*;
public class FutureGetExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
System.out.println("タスク開始");
Thread.sleep(2000);
if (true) {
throw new IllegalStateException("何かがおかしい");
}
// return 42;
});
try {
Integer result = future.get(); // ここで例外が飛ぶ
System.out.println("結果 = " + result);
} catch (InterruptedException e) {
System.out.println("待っている間に割り込まれました");
} catch (ExecutionException e) {
System.out.println("タスク内で例外: " + e.getCause());
}
executor.shutdown();
}
}
Javaタスク内で IllegalStateException を投げています。
その結果、future.get() では ExecutionException が投げられ、
e.getCause()
で「元の例外(ここでは IllegalStateException)」にたどり着けます。
ここが重要です。
タスク内の例外は、get() を呼んだときに初めて表面化する。
そのときは ExecutionException に包まれている。
というルールを、しっかり頭に入れておいてください。
タイムアウト付きの get:待ちすぎないための仕組み
get(long timeout, TimeUnit unit)
ときには「結果をいつまでも待つわけにはいかない」こともあります。
例えば、3 秒以内に結果が来なければ諦めたい、という場合。
import java.util.concurrent.*;
public class FutureTimeoutExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
Thread.sleep(5000); // 5秒かかる
return 123;
});
try {
System.out.println("3秒だけ待つ…");
Integer result = future.get(3, TimeUnit.SECONDS);
System.out.println("結果 = " + result);
} catch (TimeoutException e) {
System.out.println("タイムアウトしました。もう待ちません。");
future.cancel(true); // 後で説明
} catch (InterruptedException e) {
System.out.println("待っている間に割り込まれました");
} catch (ExecutionException e) {
System.out.println("タスク内で例外: " + e.getCause());
}
executor.shutdown();
}
}
Javaget(3, TimeUnit.SECONDS) は、
- タスクが 3 秒以内に終われば結果を返す
- 終わらなければ
TimeoutExceptionを投げる
という挙動です。
タイムアウト時にどうするかは設計次第ですが、
「キャンセルを試みる」のが一般的なパターンのひとつです(後述)。
isDone / isCancelled / cancel:タスクの状態を確認・変更する
isDone() で「終わったかどうか」だけを知る
get() は「終わるまで待つ」メソッドですが、
ただ「終わったかどうか」だけを知りたいときは isDone() を使います。
Future<Integer> future = executor.submit(() -> {
Thread.sleep(2000);
return 42;
});
while (!future.isDone()) {
System.out.println("まだ終わってない…");
Thread.sleep(500);
}
System.out.println("完了!結果 = " + future.get());
Javaこの例では、半秒おきに状態をポーリングして、isDone() が true になったら get() しています。
実務では、無限ループでポーリングするのはあまり綺麗ではありませんが、
「get() はブロックする」
「isDone() はブロックせずに状態だけ見れる」
という違いを押さえておくことが重要です。
cancel(boolean mayInterruptIfRunning) でキャンセルを試みる
Future#cancel(boolean) は、タスクのキャンセルを試みます。
boolean cancelled = future.cancel(true);
System.out.println("キャンセル成功?: " + cancelled);
Java引数の mayInterruptIfRunning には、
- まだ開始されていないタスク → 常にキャンセルされる(実行キューから外される)
- すでに実行中のタスク → true ならスレッドに割り込みをかける/false なら何もしない
という意味があります。
ただし、「キャンセルを試みる」だけであって、
実際にタスクがすぐ止まるかどうかは、そのタスクの書き方次第です。
タスク側は、
Thread.sleepなど割り込みに反応するメソッドを正しく使っている- 自分で
Thread.currentThread().isInterrupted()をチェックして早期リターンする
など、「割り込みに優しく書かれている」必要があります。
キャンセル後は、
future.isCancelled() → truefuture.isDone() → true
になります。
get() を呼ぶと CancellationException が投げられます。
isCancelled() でキャンセルされたかどうかを見る
キャンセルされたかどうかだけ知りたいときは isCancelled() を使います。
if (future.isCancelled()) {
System.out.println("このタスクはキャンセルされました");
}
JavaisDone() は「正常終了か例外かキャンセルかに関わらず、とにかく終わった」isCancelled() は「キャンセルで終わった」
という違いだと理解してください。
Runnable を submit したときの Future(戻り値なしの場合)
Future<?> の get() は何を返すのか
ExecutorService#submit(Runnable task) は Future<?> を返します。
Future<?> future = executor.submit(() -> {
System.out.println("何かするけど、戻り値はいらないタスク");
});
Javaこの場合、get() は何を返すでしょうか?
答えは「null です」。
Object result = future.get(); // null
Java戻り値は特に意味がないので、
「完了(または失敗/キャンセル)を待つため」に使うのが普通です。
たとえば、
Future<?> future = executor.submit(longRunningRunnable);
// 別のことをして…
// 最後に「終わるのを待って」から終了したい
future.get();
Javaのように、「task の完了を待つ(例外があれば ExecutionException として検知する)」
という用途で使います。
例題でまとめて流れを掴む
例:複数の Web API 呼び出しを並列に行って、一番遅いものまで待つ
疑似的な「時間のかかる API 呼び出し」を 3 つ並列に実行する例です。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class MultiApiExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Callable<String>> tasks = List.of(
() -> {
Thread.sleep(1000);
return "API-1 の結果";
},
() -> {
Thread.sleep(2000);
return "API-2 の結果";
},
() -> {
Thread.sleep(1500);
return "API-3 の結果";
}
);
List<Future<String>> futures = new ArrayList<>();
for (Callable<String> task : tasks) {
futures.add(executor.submit(task));
}
System.out.println("すべての API を並列に呼び出しました。結果を待ちます…");
for (Future<String> f : futures) {
String result = f.get(); // 1つずつ完了を待つ
System.out.println("結果: " + result);
}
executor.shutdown();
}
}
Javaこのコードでは、
- 3 つの API 呼び出しタスクをほぼ同時に開始している
- それぞれは別スレッドで走る
f.get()を順番に呼ぶことで、「全部終わるまで」待っている- 合計時間は最も遅いタスク(ここでは 2000ms)に近くなる
という挙動になります。
Future を使うことで、
「並列で投げておいて、後から順番に結果を回収する」
というパターンが、素直に表現できます。
注意点:Future で“非同期を扱っているつもり”にならない
get() をすぐ呼ぶと「ただの同期処理」に逆戻りする
よくある悪いパターンとして、
Future<Result> f = executor.submit(task);
Result r = f.get(); // すぐ呼ぶ
Javaという書き方があります。
これだと、
タスクを別スレッドに投げた直後に、
メインスレッドで「終わるまで待ち続ける」ので、
「new Thread して join する」のと本質的にあまり変わりません。
Future の良さは、
- 「今すぐ結果がいらない」場面では、投げっぱなしにできる
- 後で必要になったときにだけ、
get()で待つ - または
isDone()で状態を見ながら、他のことを並行して進める
という柔軟さにあります。
常に submit 直後に get() してしまうと、
Future の旨味を活かせていない状態になります。
「どこまで非同期で、どこから同期で受け取るか」を設計として決める
Future を使うときは、
- タスクを投げる場所
- 結果を受け取る場所
を意識的に分けて設計すると、コードが整理されます。
例えば、
- コントローラ層で全部のタスクを投げて、
- サービス層で
get()してまとめる
とか、
- サービス層でタスクを投げて、
- さらに別の集約メソッドで
get()して合成する
など、「非同期の範囲」をちゃんと決めることが大事です。
まとめ:Future を自分の中でこう位置づける
Future を初心者向けにまとめると、
「ExecutorService に投げた “将来の結果” を表す約束の箱であり、
その箱に対して『待つ/タイムアウトする/キャンセルする/状態を見る』を行うためのインターフェース」
です。
特に意識してほしいのは、
submitの戻り値としてFutureを受け取り、後からget()で結果を受け取るget()は完了までブロックし、タスク内例外はExecutionException経由で受け取るget(timeout, unit)でタイムアウト付きの待機ができるisDone/isCancelled/cancelで状態やキャンセルを制御できる- 「投げる場所」と「受け取る場所」を意識的に分けて設計すると、非同期処理が整理される
