Каковы основные различия между интерфейсами Runnable и Callable в Java?

«Каковы основные различия между интерфейсами Runnable и Callable в Java?» — вопрос из категории Java Core, который задают на 24% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Оба интерфейса представляют задачу, которую можно выполнить в отдельном потоке, но Callable является более мощной и гибкой версией, представленной в Java 5 вместе с ExecutorService.

Сравнительная таблица:

Критерий Runnable Callable<V>
Пакет java.lang java.util.concurrent
Единственный метод void run() V call() throws Exception
Возвращаемое значение Нет (void). Есть (тип V). Позволяет получить результат асинхронной операции.
Возбуждаемые исключения Не может объявлять проверяемые исключения (checked exceptions). Может объявлять и бросать проверяемые исключения.
Использование с ExecutorService Можно передать в execute(Runnable) (нет возвращаемого Future). Можно передать только в submit(Callable) или submit(Runnable, T) (возвращает Future<V>).
Использование с Thread Класс Thread принимает только Runnable. Не может быть передан напрямую в Thread.

Практические примеры:

import java.util.concurrent.*;

// 1. Пример с Runnable (устаревший низкоуровневый способ)
Runnable logTask = () -> {
    try {
        Thread.sleep(1000);
        System.out.println("Runnable task finished.");
    } catch (InterruptedException e) {
        // Обработка InterruptedException - единственное проверяемое исключение для потоков
        Thread.currentThread().interrupt();
    }
};
new Thread(logTask).start(); // Запуск в отдельном потоке

// 2. Пример с Callable и ExecutorService (современный способ)
ExecutorService executor = Executors.newSingleThreadExecutor();

Callable<String> computationTask = () -> {
    TimeUnit.SECONDS.sleep(2);
    // Можем легко бросить проверяемое исключение
    if (Math.random() > 0.5) {
        throw new IOException("Simulated I/O error");
    }
    return "Result of computation"; // Возвращаем результат
};

Future<String> future = executor.submit(computationTask); // Неблокирующий вызов

try {
    // future.get() блокирует текущий поток до завершения задачи
    String result = future.get(3, TimeUnit.SECONDS); // С таймаутом
    System.out.println("Got result: " + result);
} catch (TimeoutException e) {
    System.err.println("Task timed out");
    future.cancel(true); // Прервать задачу, если она еще выполняется
} catch (ExecutionException e) {
    // Исключение, брошенное внутри call(), обернуто в ExecutionException
    System.err.println("Task failed: " + e.getCause());
} catch (InterruptedException e) {
    System.err.println("Interrupted while waiting");
    Thread.currentThread().interrupt();
}
executor.shutdown();

Ключевой вывод: Для получения результата из фоновой задачи или обработки исключений всегда используйте Callable с ExecutorService. Runnable подходит для простых фоновых операций без возвращаемого значения.