В Java: implements Runnable против extends Thread — что выбрать?
Ключевое отличие: наследование vs композиция
Вы правы — оба подхода работают, но разница критична для архитектуры приложения. Главный принцип: предпочитайте интерфейсы наследованию (Item 18 из Effective Java).
Почему implements Runnable лучше?
- Гибкость наследования: Java не поддерживает множественное наследование классов. Если вы унаследуетесь от
Thread, ваш класс не сможет расширить другой класс (например,JFrameдля GUI). - Разделение задачи и потока:
Runnable— это задача, которую можно выполнить.Thread— это механизм выполнения. Смешивать их — плохая практика. - Повторное использование: Один
Runnableможно передать в пул потоков (ExecutorService), вThreadили даже выполнить в текущем потоке черезrun(). - Тестируемость:
Runnableлегко тестировать без создания потоков.
Конкретные примеры
Плохой пример с extends Thread:
// Нельзя расширить другой класс
class MyWindow extends Thread { // Ошибка: уже extends Thread
// Хотелось бы extends JFrame
}
Хороший пример с implements Runnable:
class MyTask implements Runnable {
@Override
public void run() {
// Логика задачи
}
}
// Использование в разных контекстах:
// 1. Вручную
new Thread(new MyTask()).start();
// 2. Через ExecutorService (рекомендуется!)
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(new MyTask());
executor.shutdown();
// 3. В текущем потоке (для тестов)
new MyTask().run();
Когда extends Thread всё же допустим?
- Если вы пишете простой учебный пример для одного класса.
- Если вам нужно переопределить другие методы
Thread(например,interrupt()илиgetName()), но это крайне редкий случай.
Важно: Никогда не смешивайте run() и start(). Вызов run() напрямую не создаёт новый поток — код выполняется в текущем потоке.
Итог
Используйте implements Runnable всегда, когда это возможно. Это не просто "правило хорошего тона", а архитектурное решение, которое делает код гибким, тестируемым и соответствующим принципам ООП. extends Thread оставьте для редких случаев, когда вы точно знаете, зачем вам нужно наследовать именно класс Thread.
Дополнение: производительность и пулы потоков
Коллега выше отлично объяснил архитектурные причины. Добавлю важный практический аспект.
Современные Java-приложения редко создают потоки через new Thread(). Вместо этого используют ExecutorService из пакета java.util.concurrent. Этот подход:
- Управляет пулом потоков (не создаёт поток на каждую задачу).
- Переиспользует потоки, снижая накладные расходы.
- Позволяет легко настраивать количество потоков, очереди и политики отказа.
// Правильный современный подход
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
System.out.println("Задача в потоке: " + Thread.currentThread().getName());
});
}
executor.shutdown();
ExecutorService принимает только Runnable и Callable (который возвращает результат). Если вы используете extends Thread, вы не сможете передать задачу в пул без дополнительных обёрток. Это ещё один сильный аргумент в пользу implements Runnable.
Также отмечу: Runnable — это функциональный интерфейс, поэтому с Java 8 можно использовать лямбды:
new Thread(() -> System.out.println("Лямбда")).start();
С extends Thread так сделать не получится.