Статья

Отличия Runnable и Callable в Java

С первых дней существования Java многопоточность была основным аспектом языка. Runnable – это основной интерфейс, предоставляемый для многопоточных задач, а в Java 1.5 появился Callable как улучшенная версия Runnable.
В этом руководстве мы рассмотрим различия и области применения обоих интерфейсов.

Механизм исполнения

Оба интерфейса предназначены для представления задачи, которая может выполняться несколькими потоками. Мы можем запускать Runnable задачи, используя класс Thread или ExecutorService, Callable объекты запускаются только с помощью ExecutorService.
Давайте подробнее рассмотрим, как эти интерфейсы обрабатывают возвращаемые значения.

Runnable

Интерфейс Runnable является функциональным интерфейсом и имеет единственный метод run(), который не принимает никаких параметров и не возвращает никаких значений. Это бывает нужно в ситуациях, когда нам не нужен результат выполнения потока, например, протоколирование входящих событий:
public interface Runnable {
    public void run();
}
Давайте посмотрим на пример:
public class LoggingTask implements Runnable {
    private Logger logger
      = LoggerFactory.getLogger(LoggingTask.class);

    @Override
    public void run() {
        logger.info("Hello");
    }
}
В этом примере поток просто читает сообщение из очереди и регистрирует его в файле журнала. Из задачи не возвращается никакого значения.

Мы можем запустить задачу с помощью ExecutorService:

В этом примере поток просто прочитает сообщение из очереди и зарегистрирует его в файле журнала. Из задачи не возвращается никакого значения.

Мы можем запустить задачу с помощью ExecutorService:
public void execute() {
    exeService = Executors.newSingleThreadExecutor();
    Future future = exeService.submit(new LoggingTask());
    exeService.shutdown();
}
В этом случае объект Future не будет иметь никакого значения.

Callable

Callable интерфейс – это generic интерфейс, содержащий один метод call(), который возвращает generic значение V:
public interface Callable<V> {
    V call() throws Exception;
}
Давайте рассмотрим вычисление факториала числа:
public class FactorialTask implements Callable<Integer> {
    int number;

    // стандартные конструкторы

    public Integer call() throws InvalidParamaterException {
        int factor = 1;
        // ...

        for(int count = number; count > 1; count--) {
            factor = factor * count;
        }

        return factor;
    }
}
Результат метода call() возвращается в Future объекте:
@Test
public void whenTaskSubmitted_ThenFutureResultObtained(){
    FactorialTask task = new FactorialTask(7);
    Future<Integer> future = executorService.submit(task);
 
    assertEquals(5040, future.get().intValue());
}

Обработка исключений

Давайте посмотрим, насколько они подходят для обработки исключений.

Runnable

Поскольку в сигнатуре метода не указан блок throws, у нас нет возможности распространять дальше проверяемые исключения.

Callable

Метод call() Callable содержит блок throws Exception, поэтому мы можем легко распространять дальше проверяемые исключения:
public class FactorialTask implements Callable<Integer> {

    public Integer call() throws InvalidParamaterException {

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

    }
}
В случае запуска Callable объекта с использованием ExecutorService исключения собираются в Future объекте. Мы можем проверить его, выполнив вызов метода Future.get().

Последует ExecutionException, которое оборачивает исходное исключение:

В случае запуска вызываемого объекта с использованием ExecutorService исключения собираются в будущем объекте. Мы можем проверить это, выполнив вызов метода Future.get().

Это вызовет ExecutionException, которое оборачивает исходное исключение:
@Test(expected = ExecutionException.class)
public void whenException_ThenCallableThrowsIt() {
 
    FactorialCallableTask task = new FactorialCallableTask(-4);
    Future<Integer> future = executorService.submit(task);
    Integer result = future.get().intValue();
}
В приведенном выше тесте выдается исключение ExecutionException, поскольку мы передаем недопустимое число. Мы можем вызвать метод getCause() для этого объекта исключения, чтобы получить исходное проверяемое исключение.

Если мы не вызовем метод get() из Future класса, исключение, вызванное методом call(), не будет возвращено, и задача по-прежнему будет помечена как выполненная:

В приведенном выше тесте выдается исключение ExecutionException, поскольку мы передаем недопустимое число. Мы можем вызвать метод getCause() для этого объекта исключения, чтобы получить исходное проверенное исключение.

Если мы не вызовем метод get() будущего класса, исключение, вызванное методом call(), не будет возвращено, и задача по-прежнему будет помечена как выполненная:
@Test
public void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){
    FactorialCallableTask task = new FactorialCallableTask(-4);
    Future<Integer> future = executorService.submit(task);
 
    assertEquals(false, future.isDone());
}
Приведенный выше тест пройдет успешно, даже несмотря на то, что мы выдали исключение для отрицательных значений параметра FactorialCallableTask.

Заключение

В этой статье мы исследовали различия между Runnable и Callable интерфейсами.
java