【Java】JavaのRunnableとCallableの違い:戻り値と例外処理から理解する

Java

こんにちは、かつコーチです。
前回はThreadの基本を扱いましたが、今回は並行処理でタスクを表現する2つのインターフェース、RunnableCallableの違いを整理します。
「どちらを使えばいいのか」で迷った経験がある方向けの内容です。

Runnableの特徴と限界

Runnableの基本形

Runnablejava.langパッケージにある関数型インターフェースで、戻り値も検査例外もないタスクを表します。

@FunctionalInterface
public interface Runnable {
    void run();
}
Runnable task = () -> {
    System.out.println("処理を実行中");
};
new Thread(task).start();

シンプルなぶん、「実行結果を受け取りたい」「処理中に発生した例外を呼び出し元に伝えたい」というケースには対応できません。

Runnableで戻り値を扱おうとしたときの壁

以前、ファイルの集計結果を別スレッドで計算し、その結果をmainスレッドに返そうとしてRunnableを使い、詰まったことがあります。

// ❌ Before:Runnableは戻り値を返せない
int[] result = new int[1]; // 苦肉の策で配列に結果を格納
Runnable task = () -> {
    result[0] = heavyCalculation();
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println(result[0]);

動くことは動くのですが、外部変数に無理やり結果を詰め込む形になり、コードの意図が読み取りにくくなります。
検査例外(IOExceptionなど)をrun()内でスローすることもできないため、try-catchで握りつぶすか実行時例外に変換する必要がありました。

Callableによる戻り値と例外の扱い

Callableの基本形

Callable<V>java.util.concurrentパッケージのインターフェースで、戻り値と検査例外のスローに対応しています。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
Callable<Integer> task = () -> {
    return heavyCalculation(); // 戻り値を返せる
};

✅ After:ExecutorServiceとCallableで戻り値を受け取る

Callableは単体ではThreadに渡せないため、ExecutorServiceと組み合わせて使うのが基本です。

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Callable<Integer> task = () -> {
            Thread.sleep(500);
            return heavyCalculation();
        };

        Future<Integer> future = executor.submit(task);
        Integer result = future.get(); // 結果を待って受け取る
        System.out.println("計算結果: " + result);

        executor.shutdown();
    }

    static int heavyCalculation() {
        return 42;
    }
}

future.get()によって戻り値を自然に受け取れるようになり、外部変数に頼る必要がなくなりました。
またcall()が検査例外をスローできるため、Exceptionをそのまま宣言できます。

例外処理の違いを比較する

項目RunnableCallable
戻り値なし(void)あり(ジェネリクスで型指定)
検査例外のスロー不可(実行時例外のみ)可能(throws Exception
主な利用先ThreadExecutorService.execute()ExecutorService.submit()
結果の受け取り方外部変数など迂回が必要Future<V>で受け取る

判断軸はシンプルで、戻り値や検査例外が必要ならCallable、不要ならRunnableです。
実務では「計算結果が欲しい」「I/O処理で例外が発生しうる」ケースが多いため、Callableを使う場面の方が増えていきます。

FutureでCallableの結果を扱う際の注意点

getはブロッキング処理

future.get()は結果が返るまで呼び出し元のスレッドをブロックします。
タイムアウトを指定しないと、タスクが終わらない限り永遠に待ち続けるリスクがあるため、実務では以下のようにタイムアウト付きで呼び出すことをおすすめします。

try {
    Integer result = future.get(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    System.out.println("処理がタイムアウトしました");
    future.cancel(true);
}

まとめ

この記事のポイント

  • Runnableは戻り値なし・検査例外なしのシンプルなタスク表現
  • Callableは戻り値ありでジェネリクスに対応し、検査例外もスローできる
  • CallableExecutorService.submit()と組み合わせてFutureで結果を受け取る
  • future.get()はブロッキング処理なので、実務ではタイムアウトを指定する

次に読むべき記事

  • スレッドの基本を復習したい方は「Threadの基本」の記事へ
  • ExecutorServiceの詳しい使い方は「ExecutorServiceでスレッドプールを使う」の記事へ

タグ: Java, 中級者向け, 並行処理

タイトルとURLをコピーしました