Интерфейс или абстрактный класс?
Один из самых частых вопросов джуниор- и мидл-собеседований — и почти всегда с подвохом: «а зачем нам вообще два механизма, если оба про наследование поведения?»
Вопрос с собеседования: «Когда вы выберете интерфейс, а когда — абстрактный класс? И что изменилось после появления default-методов в интерфейсах?»
Формально в Java оба инструмента позволяют описать «контракт», которому должны следовать классы-наследники. Но правильный ответ — не «оба варианта работают», а понимание конкретных ограничений каждого.
Что выведет этот код?
Сначала посмотрим на код, который компилируется без единой ошибки, — и разберёмся, почему.
interface Flyable {
void fly(); // абстрактный метод — реализации нет
default void announce() { // default-метод — реализация есть прямо в интерфейсе
System.out.println("Приготовиться к полёту!");
}
}
abstract class Bird {
String name;
Bird(String name) { this.name = name; }
abstract void makeSound(); // абстрактный метод — тела нет
void sleep() { // обычный метод — тело есть
System.out.println(name + " спит.");
}
}
class Eagle extends Bird implements Flyable {
Eagle(String name) { super(name); }
@Override
void makeSound() {
System.out.println(name + " кричит: Клекочет!");
}
@Override
public void fly() {
System.out.println(name + " взлетает.");
}
}
public class Main {
public static void main(String[] args) {
Eagle eagle = new Eagle("Орёл");
eagle.makeSound();
eagle.sleep();
eagle.announce();
eagle.fly();
}
}Вывод:
Орёл кричит: Клекочет!
Орёл спит.
Приготовиться к полёту!
Орёл взлетает.Класс Eagle одновременно наследует состояние и часть поведения от абстрактного класса Bird (поле name, готовый метод sleep()) и реализует контракт интерфейса Flyable — при этом бесплатно получает метод announce(), который вообще не пришлось переопределять. Это и есть ключ к вопросу: интерфейс и абстрактный класс решают разные задачи, и здесь понадобились оба сразу.
Как это работает под капотом
Абстрактный класс — это обычный класс (abstract class), у которого может быть состояние (поля), конструктор, обычные методы с телом и абстрактные методы без тела, которые обязан реализовать первый неабстрактный наследник. Использовать new напрямую для него нельзя — только через подкласс. Главное ограничение: у класса в Java может быть только один родитель, поэтому наследовать сразу два абстрактных класса невозможно.
Интерфейс исторически был чистым контрактом — набором сигнатур методов без реализации и без состояния (только константы public static final). Класс мог реализовывать (implements) сколько угодно интерфейсов одновременно — это и есть подобие «множественного наследования», недоступное для классов.
С Java 8 в интерфейсах появились default-методы — методы с готовой реализацией прямо внутри интерфейса, помеченные ключевым словом default. Это стёрло часть границы: теперь интерфейс тоже может «раздавать» готовое поведение, а не только требовать его реализации. Придумали default-методы не ради архитектурной моды, а по конкретной причине: разработчики Java хотели добавить новые методы в существующие интерфейсы стандартной библиотеки (например, forEach() в Collection), не ломая при этом тысячи уже написанных классов, которые эти интерфейсы реализуют. Без default-метода добавление нового абстрактного метода в интерфейс заставило бы переписывать весь код, который его реализует.
Множественное наследование интерфейсов: что будет при конфликте?
Раз класс может реализовывать несколько интерфейсов, а у каждого может быть default-метод с одинаковой сигнатурой — что произойдёт при столкновении?
interface Swimmer {
default void move() {
System.out.println("Плыву brasом.");
}
}
interface Runner {
default void move() {
System.out.println("Бегу трусцой.");
}
}
class Triathlete implements Swimmer, Runner {
@Override
public void move() { // ОБЯЗАН переопределить — иначе ошибка компиляции
Swimmer.super.move(); // явный выбор: какой из default-методов вызвать
Runner.super.move();
System.out.println("Триатлон завершён.");
}
}
public class Main {
public static void main(String[] args) {
new Triathlete().move();
}
}Вывод:
Плыву brasом.
Бегу трусцой.
Триатлон завершён.Если убрать переопределение move() в Triathlete, код вообще не скомпилируется — компилятор Java не может сам решить, какой из двух одинаковых default-методов «главнее», и заставляет программиста разрешить конфликт явно. Именно синтаксис ИмяИнтерфейса.super.метод() позволяет внутри переопределённого метода вызвать конкретную default-реализацию нужного интерфейса.
Практическое правило выбора
На практике эти два инструмента редко конкурируют «или-или» — чаще они дополняют друг друга, как в первом примере с Bird и Flyable. Абстрактный класс отвечает на вопрос «что представляет собой объект» (иерархия «это разновидность»), а интерфейс — на вопрос «что объект умеет делать» (способность, не привязанная к конкретной иерархии наследования).
| Признак ситуации | Что выбрать |
| Нужно общее состояние (поля) и общий код в конструкторе для группы похожих классов | Абстрактный класс |
Классы из совсем разных иерархий должны уметь одно и то же (например, и Bird, и Airplane умеют fly()) | Интерфейс |
| Нужно «подмешать» одну способность в разные, не связанные между собой классы | Интерфейс (можно реализовать сразу несколько) |
| Хотите зафиксировать жёсткую иерархию «это разновидность того-то» | Абстрактный класс |
Показательный пример из стандартной библиотеки Java: и ArrayList, и LinkedList реализуют интерфейс List (контракт «умею быть списком»), но при этом происходят от разных абстрактных классов (AbstractList и AbstractSequentialList соответственно), которые уже дают им часть готовой реализации, общей для похожих по устройству списков.
Частые ошибки на собеседовании
- Говорить, что интерфейсы «заменили» абстрактные классы после Java 8. Это не так: у интерфейса по-прежнему нет полей состояния (кроме констант) и нет конструктора — default-методы дали только готовое поведение, но не состояние.
- Путать «может быть» и «должен быть» абстрактным. В абстрактном классе не все методы обязаны быть абстрактными — там могут спокойно соседствовать готовые и незаконченные методы. А вот если класс не абстрактный, у него абстрактных методов быть не может вообще.
- Забывать про конфликт default-методов. Если кандидат не может объяснить, что произойдёт при реализации двух интерфейсов с одинаковым default-методом, — это явный пробел, который проверяющие любят находить.
- Утверждать, что интерфейс может хранить обычные поля. Поля в интерфейсе автоматически становятся
public static final— то есть константами, а не изменяемым состоянием экземпляра.
Итоги-шпаргалка
- Абстрактный класс — для общего состояния и части готового кода внутри одной иерархии «это разновидность». Наследуется только один.
- Интерфейс — для контракта поведения, который может пригодиться классам из разных, не связанных иерархий. Реализовать можно сколько угодно сразу.
- Default-методы (с Java 8) дали интерфейсам готовую реализацию — но не состояние (поля) и не конструктор.
- При конфликте двух default-методов с одинаковой сигнатурой класс обязан переопределить метод сам, при необходимости выбирая нужную реализацию через
Интерфейс.super.метод().