1. 概要

Javaの初期の頃から、マルチスレッドは言語の主要な側面でした。 Runnable は、マルチスレッドタスクを表すために提供されるコアインターフェイスであり、Java 1.5は、Runnableの改良版としてCallableを提供しました。

このチュートリアルでは、両方のインターフェースの違いとアプリケーションについて説明します。

2. 実行メカニズム

両方のインターフェースは、複数のスレッドで実行できるタスクを表すように設計されています。 RunnableタスクはThreadクラスまたはExecutorServiceを使用して実行できますが、Callableは後者を使用してのみ実行できます。

3. 戻り値

これらのインターフェースが戻り値を処理する方法を詳しく見ていきましょう。

3.1. 実行可能を使用

Runnable インターフェースは機能的なインターフェースであり、パラメーターを受け入れたり値を返したりしない単一の run()メソッドを備えています。

これは、着信イベントのログ記録など、スレッド実行の結果を探していない状況で機能します。

public interface Runnable {
    public void run();
}

例を使ってこれを理解しましょう:

public class EventLoggingTask implements  Runnable{
    private Logger logger
      = LoggerFactory.getLogger(EventLoggingTask.class);

    @Override
    public void run() {
        logger.info("Message");
    }
}

この例では、スレッドはキューからメッセージを読み取り、それをログファイルに記録します。 タスクから返される値はありません。

ExecutorServiceを使用してタスクを起動できます。

public void executeTask() {
    executorService = Executors.newSingleThreadExecutor();
    Future future = executorService.submit(new EventLoggingTask());
    executorService.shutdown();
}

この場合、Futureオブジェクトは値を保持しません。

3.2. 呼び出し可能を使用

Callable インターフェースは、ジェネリック値 Vを返す単一のcall()メソッドを含むジェネリックインターフェースです。

public interface Callable<V> {
    V call() throws Exception;
}

数の階乗の計算を見てみましょう。

public class FactorialTask implements Callable<Integer> {
    int number;

    // standard constructors

    public Integer call() throws InvalidParamaterException {
        int fact = 1;
        // ...
        for(int count = number; count > 1; count--) {
            fact = fact * count;
        }

        return fact;
    }
}

call()メソッドの結果は、Futureオブジェクト内に返されます。

@Test
public void whenTaskSubmitted_ThenFutureResultObtained(){
    FactorialTask task = new FactorialTask(5);
    Future<Integer> future = executorService.submit(task);
 
    assertEquals(120, future.get().intValue());
}

4. 例外処理

それらが例外処理にどれほど適しているか見てみましょう。

4.1. 実行可能を使用

メソッドシグニチャには「throws」句が指定されていないため、さらにチェックされた例外を伝播する方法がありません。

4.2. 呼び出し可能を使用

Callablecall()メソッドには、「throws Exception 」句が含まれているため、チェックされた例外をさらに簡単に伝播できます。

public class FactorialTask implements Callable<Integer> {
    // ...
    public Integer call() throws InvalidParamaterException {

        if(number < 0) {
            throw new InvalidParamaterException("Number should be positive");
        }
    // ...
    }
}

ExecutorServiceを使用してCallableを実行する場合、例外はFutureオブジェクトに収集されます。 これは、 Future.get()メソッドを呼び出すことで確認できます。

これにより、 ExecutionException がスローされ、元の例外がラップされます。

@Test(expected = ExecutionException.class)
public void whenException_ThenCallableThrowsIt() {
 
    FactorialCallableTask task = new FactorialCallableTask(-5);
    Future<Integer> future = executorService.submit(task);
    Integer result = future.get().intValue();
}

上記のテストでは、無効な番号を渡したため、ExecutionExceptionがスローされます。 この例外オブジェクトでgetCause()メソッドを呼び出して、元のチェックされた例外を取得できます。

Futureクラスのget()メソッドを呼び出さないと、 call()メソッドによってスローされた例外は報告されません。タスクは引き続き完了としてマークされます。

@Test
public void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){
    FactorialCallableTask task = new FactorialCallableTask(-5);
    Future<Integer> future = executorService.submit(task);
 
    assertEquals(false, future.isDone());
}

FactorialCallableTask へのパラメーターの負の値の例外をスローした場合でも、上記のテストは正常に合格します。

5. 結論

この記事では、実行可能インターフェースと呼び出し可能インターフェースの違いについて説明しました。

いつものように、この記事の完全なコードは、GitHubから入手できます。