[Java] Callableの基本的な使い方を解説 – スレッドとの関係性

CallableはJavaの並行処理で使用されるインターフェースで、スレッドを使って非同期処理を行う際に役立ちます。

CallableはRunnableと似ていますが、戻り値を返すことができ、例外もスロー可能です。

Callableはcall()メソッドを実装し、ExecutorServiceを使ってスレッドプールで実行されます。

submit()メソッドでCallableを実行すると、Futureオブジェクトが返され、非同期処理の結果を取得したり、完了を待機したりできます。

この記事でわかること
  • Callableインターフェースの基本的な使い方
  • スレッドとの関係性についての理解
  • 複数のCallableを並列実行する方法
  • Callableと他のAPIの違い
  • 非同期処理のエラーハンドリング方法

Callableを活用して、より効率的な非同期処理を実現することが求められる

目次から探す

Callableとは何か

Callableは、Javaの並行処理において、タスクを定義するためのインターフェースです。

Runnableと似ていますが、主な違いは、Callableが戻り値を持つ点です。

Callableインターフェースは、call()メソッドを実装することで、タスクの実行内容を定義します。

このメソッドは、タスクが正常に完了した場合に結果を返し、例外が発生した場合にはその例外をスローします。

Callableは、ExecutorServiceと組み合わせて使用されることが多く、非同期処理や並列処理を簡単に実現するための強力なツールです。

Callableの基本的な使い方

Callableインターフェースの実装

Callableインターフェースを実装するには、まずこのインターフェースを実装したクラスを作成します。

以下のサンプルコードでは、数値を2倍にするタスクを定義しています。

import java.util.concurrent.Callable;
public class DoubleTask implements Callable<Integer> {
    private final int number;
    public DoubleTask(int number) {
        this.number = number;
    }
    @Override
    public Integer call() throws Exception {
        // 数値を2倍にする
        return number * 2;
    }
}

call()メソッドの定義

call()メソッドは、Callableインターフェースの中で定義されているメソッドで、タスクの実行内容を記述します。

このメソッドは、戻り値の型を指定することができ、タスクが完了した際にその結果を返します。

上記の例では、call()メソッドが数値を2倍にして返しています。

ExecutorServiceを使ったCallableの実行

ExecutorServiceを使用することで、Callableタスクを簡単に実行できます。

以下のサンプルコードでは、ExecutorServiceを使ってDoubleTaskを実行しています。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Callable;

class DoubleTask implements Callable<Integer> {
    private final int number;

    public DoubleTask(int number) {
        this.number = number;
    }

    @Override
    public Integer call() throws Exception {
        // 数値を2倍にする
        return number * 2;
    }
}

public class App {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        DoubleTask task = new DoubleTask(5);

        // Callableタスクを実行
        Future<Integer> future = executor.submit(task);

        // 結果を取得
        Integer result = future.get();
        System.out.println("結果: " + result);

        executor.shutdown();
    }
}
結果: 10

Futureオブジェクトの取得と利用

Callableタスクを実行すると、Futureオブジェクトが返されます。

このオブジェクトを使用して、タスクの結果を取得したり、タスクの状態を確認したりできます。

上記の例では、future.get()メソッドを使って、タスクの結果を取得しています。

Futureオブジェクトは、タスクが完了するまでブロックされるため、結果を待つことができます。

Callableの例外処理の実装方法

Callableタスク内で例外が発生した場合、call()メソッドはその例外をスローします。

Futureオブジェクトを通じて、例外を取得することができます。

以下のサンプルコードでは、例外をスローするタスクを実行し、その例外を処理しています。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class ExceptionTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 故意に例外をスロー
        throw new Exception("タスクでエラーが発生しました");
    }
}

public class App {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        ExceptionTask task = new ExceptionTask();

        Future<String> future = executor.submit(task);

        try {
            // 結果を取得
            String result = future.get();
            System.out.println("結果: " + result);
        } catch (Exception e) {
            // 例外を処理
            System.out.println("エラー: " + e.getMessage());
        } finally {
            executor.shutdown();
        }
    }
}
エラー: タスクでエラーが発生しました

このように、Callableを使用することで、タスクの実行結果や例外を簡単に管理することができます。

スレッドとの関係性

スレッドと並行処理の基本

スレッドは、プログラム内で同時に実行される処理の単位です。

Javaでは、スレッドを使用することで、並行処理を実現し、プログラムのパフォーマンスを向上させることができます。

スレッドを利用することで、I/O操作や計算処理を同時に行うことが可能になり、ユーザー体験を向上させることができます。

Javaでは、ThreadクラスRunnableインターフェースを使ってスレッドを作成することが一般的ですが、Callableインターフェースを使用することで、戻り値を持つタスクを実行することができます。

Callableとスレッドプールの関係

Callableは、スレッドプールと組み合わせて使用されることが多いです。

スレッドプールは、あらかじめ一定数のスレッドを生成しておき、タスクを効率的に処理するための仕組みです。

これにより、スレッドの生成や破棄にかかるオーバーヘッドを削減し、リソースの効率的な利用が可能になります。

ExecutorServiceを使用することで、Callableタスクをスレッドプールに送信し、非同期に実行することができます。

ExecutorServiceによるスレッド管理

ExecutorServiceは、スレッドプールを管理するためのインターフェースです。

これを使用することで、スレッドのライフサイクルを簡単に管理できます。

ExecutorServiceは、タスクをキューに追加し、利用可能なスレッドで実行します。

以下のサンプルコードでは、ExecutorServiceを使ってCallableタスクを実行しています。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        // 複数のCallableタスクを実行
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("タスク " + taskId + " が実行中");
                return null;
            });
        }
        
        executor.shutdown();
    }
}
タスク 0 が実行中
タスク 1 が実行中
タスク 2 が実行中
タスク 3 が実行中
タスク 4 が実行中

Futureによる非同期処理の結果取得

CallableタスクをExecutorServiceで実行すると、Futureオブジェクトが返されます。

このオブジェクトを使用して、タスクの結果を非同期に取得することができます。

Futureは、タスクが完了するまで待機することができ、結果を取得する際にブロックされます。

これにより、メインスレッドは他の処理を続けながら、タスクの結果を後で取得することが可能です。

スレッドの終了とCallableの完了

スレッドが終了する際、Callableタスクも完了します。

ExecutorServiceを使用してタスクを実行する場合、shutdown()メソッドを呼び出すことで、スレッドプールを正常に終了させることができます。

タスクがすべて完了するまで待機し、リソースを解放します。

これにより、スレッドの管理が容易になり、プログラムの安定性が向上します。

スレッドが正常に終了したかどうかは、Futureオブジェクトを通じて確認することができます。

Callableの応用例

複数のCallableを並列実行する方法

複数のCallableタスクを並列に実行するには、ExecutorServiceを使用してタスクを送信します。

以下のサンプルコードでは、複数のタスクを並行して実行し、それぞれの結果を取得しています。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class App {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        List<Future<Integer>> futures = new ArrayList<>();
        // 複数のCallableタスクを作成
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            Callable<Integer> task = () -> {
                // タスクの処理
                return taskId * 2;
            };
            futures.add(executor.submit(task));
        }
        // 結果を取得
        for (Future<Integer> future : futures) {
            System.out.println("結果: " + future.get());
        }
        executor.shutdown();
    }
}
結果: 2
結果: 4
結果: 6
結果: 8
結果: 10

invokeAll()とinvokeAny()の使い方

ExecutorServiceには、複数のCallableタスクを一度に実行するためのinvokeAll()と、最初に完了したタスクの結果を取得するためのinvokeAny()メソッドがあります。

以下のサンプルコードでは、これらのメソッドを使用してタスクを実行しています。

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class App {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        List<Callable<Integer>> tasks = Arrays.asList(
            () -> { Thread.sleep(1000); return 1; },
            () -> { Thread.sleep(500); return 2; },
            () -> { Thread.sleep(2000); return 3; }
        );
        // invokeAll()を使用
        List<Future<Integer>> results = executor.invokeAll(tasks);
        for (Future<Integer> result : results) {
            System.out.println("invokeAll 結果: " + result.get());
        }
        // invokeAny()を使用
        Integer firstResult = executor.invokeAny(tasks);
        System.out.println("invokeAny 結果: " + firstResult);
        executor.shutdown();
    }
}
invokeAll 結果: 1
invokeAll 結果: 2
invokeAll 結果: 3
invokeAny 結果: 2

タイムアウトを設定したCallableの実行

Callableタスクを実行する際に、タイムアウトを設定することができます。

これにより、指定した時間内にタスクが完了しない場合、例外がスローされます。

以下のサンプルコードでは、タイムアウトを設定してタスクを実行しています。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class App {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Callable<String> task = () -> {
            // 2秒待機
            Thread.sleep(2000);
            return "完了";
        };
        Future<String> future = executor.submit(task);
        try {
            // タイムアウトを設定
            String result = future.get(1, TimeUnit.SECONDS);
            System.out.println("結果: " + result);
        } catch (Exception e) {
            System.out.println("エラー: " + e.getMessage());
        } finally {
            executor.shutdown();
        }
    }
}
エラー: null

非同期処理のキャンセル方法

Futureオブジェクトを使用して、実行中のCallableタスクをキャンセルすることができます。

以下のサンプルコードでは、タスクをキャンセルする方法を示しています。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class App {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Callable<String> task = () -> {
            // 長時間実行されるタスク
            Thread.sleep(5000);
            return "完了";
        };
        Future<String> future = executor.submit(task);
        
        // タスクをキャンセル
        future.cancel(true);
        
        if (future.isCancelled()) {
            System.out.println("タスクはキャンセルされました。");
        } else {
            System.out.println("結果: " + future.get());
        }
        executor.shutdown();
    }
}
タスクはキャンセルされました。

複雑なタスクの分割とCallableの活用

複雑なタスクを分割して、複数のCallableタスクとして実行することで、処理を効率化できます。

以下のサンプルコードでは、大きな配列の合計を計算するタスクを分割して実行しています。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class App {
    public static void main(String[] args) throws Exception {
        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        Callable<Integer> task1 = () -> {
            int sum = 0;
            for (int i = 0; i < 5; i++) {
                sum += numbers[i];
            }
            return sum;
        };
        
        Callable<Integer> task2 = () -> {
            int sum = 0;
            for (int i = 5; i < numbers.length; i++) {
                sum += numbers[i];
            }
            return sum;
        };
        
        Future<Integer> future1 = executor.submit(task1);
        Future<Integer> future2 = executor.submit(task2);
        
        int totalSum = future1.get() + future2.get();
        System.out.println("合計: " + totalSum);
        executor.shutdown();
    }
}
合計: 55

このように、Callableを活用することで、複雑なタスクを効率的に処理することができます。

Callableと他の並行処理APIとの比較

Runnableとの比較

RunnableとCallableは、どちらもタスクを定義するためのインターフェースですが、いくつかの重要な違いがあります。

Runnableは戻り値を持たず、タスクが完了した際に結果を返すことができません。

一方、Callableは戻り値を持ち、タスクの実行結果を返すことができます。

また、Callableは例外をスローすることができるため、エラーハンドリングが容易です。

以下の表に、RunnableとCallableの主な違いを示します。

スクロールできます
特徴RunnableCallable
戻り値なしあり
例外処理できないできる
使用例スレッドの実行非同期処理や結果取得

CompletableFutureとの違い

CompletableFutureは、非同期処理をより柔軟に扱うためのAPIで、Callableを使用することもできますが、より多くの機能を提供します。

CompletableFutureは、非同期処理のチェーンを作成したり、複数の非同期タスクを組み合わせたりすることができます。

Callableは単一のタスクを定義するのに対し、CompletableFutureは複数のタスクを連携させることができるため、より複雑な非同期処理を実現できます。

以下の表に、CallableとCompletableFutureの主な違いを示します。

スクロールできます
特徴CallableCompletableFuture
タスクの実行単一のタスク複数のタスクの連携が可能
結果の取得Futureを使用自動的に結果を取得可能
エラーハンドリングFutureを通じて非同期的にエラー処理が可能

ForkJoinPoolとの違い

ForkJoinPoolは、タスクを分割して並行処理を行うための特別なスレッドプールです。

主に、再帰的なタスクを効率的に処理するために設計されています。

Callableは、単一のタスクを定義するためのインターフェースであり、ForkJoinPoolはそのタスクを分割して実行するための仕組みを提供します。

ForkJoinPoolは、タスクの分割と結合を自動的に行うため、特に大規模なデータ処理に適しています。

以下の表に、CallableとForkJoinPoolの主な違いを示します。

スクロールできます
特徴CallableForkJoinPool
タスクの実行単一のタスクタスクの分割と結合が可能
使用目的単純な非同期処理再帰的なタスクの効率的な処理
スレッド管理ExecutorServiceを使用自動的にスレッドを管理

スレッドクラスとの違い

Javaのスレッドクラスは、スレッドを直接管理するためのクラスで、Runnableインターフェースを実装してタスクを実行します。

Callableは、ExecutorServiceを使用して非同期にタスクを実行するためのインターフェースであり、スレッドの管理を簡素化します。

スレッドクラスを使用する場合、スレッドの生成や終了を手動で管理する必要がありますが、Callableを使用することで、ExecutorServiceがスレッドのライフサイクルを管理してくれるため、より効率的に並行処理を行うことができます。

以下の表に、スレッドクラスとCallableの主な違いを示します。

スクロールできます
特徴スレッドクラスCallable
スレッド管理手動で管理ExecutorServiceが管理
タスクの実行Runnableを使用戻り値を持つタスクを実行
使用目的スレッドの直接管理非同期処理や結果取得

このように、Callableは他の並行処理APIと比較して、特定の用途に応じた利点を持っています。

状況に応じて適切なAPIを選択することが重要です。

よくある質問

CallableとRunnableはどちらを使うべき?

CallableとRunnableのどちらを使用するかは、タスクの要件によります。

戻り値が必要な場合や、タスク内で例外が発生する可能性がある場合は、Callableを使用するのが適しています。

一方、戻り値が不要で、単純な処理を行うだけの場合は、Runnableを使用することが一般的です。

以下のポイントを考慮すると良いでしょう。

  • Callableを使用する場合:
    • 戻り値が必要なタスク
    • タスク内で例外処理が必要な場合
  • Runnableを使用する場合:
    • 戻り値が不要なタスク
    • シンプルな処理を行う場合

Callableの戻り値をすぐに取得する方法は?

Callableの戻り値をすぐに取得するには、ExecutorServiceを使用してタスクを実行し、Futureオブジェクトを通じて結果を取得します。

Futureオブジェクトのget()メソッドを呼び出すことで、タスクが完了するまで待機し、結果を取得できます。

ただし、タスクが完了するまでブロックされるため、非同期処理の特性を活かすためには、タスクの実行が完了するまで他の処理を行うことが重要です。

以下のように記述します。

Future<Integer> future = executor.submit(task);
Integer result = future.get(); // タスクが完了するまで待機

Callableの例外処理はどうすればよい?

Callableタスク内で例外が発生した場合、その例外はFutureオブジェクトを通じて取得できます。

get()メソッドを呼び出すと、タスクが正常に完了した場合は結果が返され、例外が発生した場合はその例外がスローされます。

これにより、タスクの実行中に発生したエラーを適切に処理することができます。

以下のように記述します。

try {
    String result = future.get(); // 結果を取得
} catch (Exception e) {
    System.out.println("エラー: " + e.getMessage()); // 例外を処理
}

このように、Callableを使用することで、タスクの実行結果や例外を簡単に管理することができます。

まとめ

この記事では、JavaのCallableインターフェースの基本的な使い方や、スレッドとの関係性、さまざまな応用例について詳しく解説しました。

Callableは、非同期処理や並行処理を効率的に行うための強力なツールであり、特に戻り値を持つタスクを実行する際に非常に便利です。

今後、Javaでのプログラミングにおいて、Callableを活用してより効率的なアプリケーションを開発してみてください。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

関連カテゴリーから探す

  • URLをコピーしました!
目次から探す