Интерфейс или абстрактный класс: чем отличаются и что выбрать
Суть вопроса
Вопрос звучит просто, но проверяет сразу три вещи: понимаешь ли ты модель наследования в Java, знаешь ли эволюцию интерфейсов после Java 8 и умеешь ли применить это на дизайне, а не только процитировать определение. Формально и абстрактный класс, и интерфейс — механизмы абстракции, но они решают разные задачи: абстрактный класс отвечает на вопрос «что это такое» (is-a со своей реализацией), интерфейс — на вопрос «что оно умеет делать» (контракт поведения).
Ключевые технические отличия
- Наследование: класс может наследовать только один абстрактный класс (одиночное наследование), но реализовать сколько угодно интерфейсов.
- Состояние: абстрактный класс может иметь поля с состоянием и конструктор. Интерфейс — только константы (public static final) и с Java 9 — private методы для внутренней переиспользуемой логики, но никакого instance-состояния.
- Методы: в абстрактном классе можно смешивать реализованные и абстрактные методы. В интерфейсе с Java 8 появились default и static методы с телом, но это не отменяет того, что интерфейс не хранит состояние.
- Модификаторы доступа: методы абстрактного класса могут быть private, protected, public. Методы интерфейса по умолчанию public (кроме private-методов для внутреннего использования).
- Конструктор: у абстрактного класса есть конструктор, вызываемый через super() из наследника. У интерфейса конструктора нет и быть не может — это не часть иерархии инициализации объекта.
Default-методы и diamond problem
После Java 8 граница стёрлась сильнее, чем кажется на первый взгляд, и это любимая ловушка на собесе: «а если интерфейс может иметь реализацию, зачем вообще абстрактные классы?». Ответ — из-за состояния и одиночного наследования классов. Default-методы решают проблему эволюции API (можно добавить метод в интерфейс без поломки всех реализаций), но не решают задачу хранения общих полей.
Отдельно стоит вопрос про конфликт default-методов при множественном наследовании интерфейсов:
interface A {
default String greet() { return "A"; }
}
interface B {
default String greet() { return "B"; }
}
class C implements A, B {
// обязаны переопределить, иначе ошибка компиляции
public String greet() {
return A.super.greet() + B.super.greet();
}
}
Java не выбирает "победителя" сама — компилятор требует явного разрешения конфликта. Это осознанное решение авторов языка: неявный diamond problem, как в C++, был бы источником трудноуловимых багов.
Как выбирать на практике
Абстрактный класс подходит, когда у наследников есть общее состояние и общая часть логики, которую бессмысленно копировать (шаблонный метод, Template Method паттерн). Интерфейс — когда важен именно контракт поведения, который могут реализовать совершенно разные по природе классы: Comparable, Runnable, Serializable — у File и у Thread нет ничего общего по иерархии, но оба могут быть Runnable/Comparable.
Практическое правило, которое хорошо звучит на собеседовании: если сомневаешься — начинай с интерфейса. Java поддерживает только одиночное наследование классов, и жёсткая привязка к абстрактному классу ограничивает наследника навсегда. Интерфейсов можно реализовать сколько угодно, это дешевле для будущей расширяемости.