Перейти к основному содержимому

Runnable против Callable в Java

· 3 мин. чтения

1. Обзор

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

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

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 — это универсальный интерфейс, содержащий единственный метод call() , который возвращает универсальное значение V :

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. С возможностью вызова

` Метод call() Callable содержит предложение « throws Exception` », поэтому мы можем легко распространять проверенные исключения дальше:

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

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

В случае запуска Callable с помощью ExecutorService исключения собираются в объекте 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() для этого объекта исключения, чтобы получить исходное проверенное исключение.

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

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

assertEquals(false, future.isDone());
}

Приведенный выше тест пройдет успешно, несмотря на то, что мы создали исключение для отрицательных значений параметра в FactorialCallableTask .

5. Вывод

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

Как всегда, полный код для этой статьи доступен на GitHub .