JavaJuniorTechnical
Объясните различия между абстрактными классами и интерфейсами в Java 17+.
Абстрактный класс даёт частичную реализацию и состояние; интерфейс — контракт без состояния. Класс может реализовывать много интерфейсов, но наследовать только один класс. С Java 8+ интерфейсы поддерживают default/static-методы.
Ключевое отличие
Абстрактный класс предназначен для is-a-иерархии с общим состоянием и частичной реализацией. Интерфейс описывает роль или способность — контракт без полей экземпляра. Класс наследует ровно один абстрактный класс, но реализует произвольное число интерфейсов.
Что разрешено где
| Abstract class | Interface (Java 8+) | |
|---|---|---|
| Поля экземпляра | да | нет (только static final) |
| Конструктор | да | нет |
| default-методы | — | да |
| static-методы | да | да (Java 8+) |
| private-методы | да | да (Java 9+) |
| sealed-иерархии | да (Java 17) | да (Java 17) |
Пример: abstract class
// Общее состояние и шаблонный метод
public abstract class HttpHandler {
private final String basePath; // поле экземпляра
protected HttpHandler(String basePath) {
this.basePath = basePath;
}
// Шаблонный метод — общий алгоритм
public final Response handle(Request req) {
validate(req); // общий шаг
return process(req); // делегируем подклассу
}
protected abstract Response process(Request req);
private void validate(Request req) {
if (!req.path().startsWith(basePath))
throw new IllegalArgumentException("Wrong path: " + req.path());
}
}
// Конкретная реализация
public class UserHandler extends HttpHandler {
public UserHandler() { super("/api/users"); }
@Override
protected Response process(Request req) {
return Response.ok("user data");
}
}
Пример: interface + default-метод
// Роль — объект умеет логировать себя
public interface Auditable {
String auditId();
// default-метод — поведение по умолчанию без реализации в классе
default String auditEntry() {
return "[AUDIT] " + auditId() + " at " + Instant.now();
}
}
// Один класс может носить несколько ролей
public class Order implements Auditable, Serializable {
private final UUID id;
public Order(UUID id) { this.id = id; }
@Override
public String auditId() { return id.toString(); }
}
// Использование
Order o = new Order(UUID.randomUUID());
System.out.println(o.auditEntry()); // [AUDIT] 3fa8… at 2026-05-19T…
Sealed-иерархии (Java 17)
// Закрытая иерархия — и abstract class, и interface могут быть sealed
public sealed interface Shape permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
// Компилятор гарантирует полноту switch
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
};
Когда выбирать что
- Есть общее состояние или шаблонный метод — abstract class.
- Нужна множественная «роль» без состояния — interface.
- Хотите закрытую иерархию для pattern matching — sealed interface + records.
- Ретрофит в существующую иерархию (добавить поведение без breaking change) — default-метод интерфейса.
Подводные камни
- Diamond problem с default-методами. Если два интерфейса объявляют одноимённый default-метод, класс обязан явно переопределить его, иначе компиляция упадёт с ошибкой.
- Нарушение контракта equals/hashCode. Абстрактный класс с полями может неявно менять контракт equals; подкласс, добавивший поля, сломает симметрию.
- Злоупотребление abstract class вместо composition. Глубокая иерархия (3+ уровней) трудно тестируется; часто лучше делегировать через поле.
- Интерфейс с единственным методом — не всегда @FunctionalInterface. Добавление второго абстрактного метода ломает все лямбды-реализации во всём коде без предупреждения (разве что аннотация есть).
- sealed + non-sealed в одной иерархии. Разрешение
non-sealedоткрывает «дыру» в замкнутой иерархии — компилятор больше не гарантирует полноту switch. - private-методы интерфейса (Java 9) не наследуются. Их нельзя вызвать из реализующего класса — только из default/static-методов того же интерфейса.
- Абстрактный класс без абстрактных методов. Это легально, но сбивает с толку: выглядит как обычный класс, но его нельзя инстанциировать напрямую.
- Смешивание ролей через интерфейсы без меры. Класс, реализующий 10 интерфейсов — God Object; каждый интерфейс должен нести единственную ответственность (ISP).
Common mistakes
- Путать термин «abstract class interface» с соседним механизмом Java.
- Не называть границу lifecycle, transaction, thread или request для «abstract class interface».
- Игнорировать production-эффекты «abstract class interface»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «abstract class interface» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «abstract class interface».
- Уточнить, какие настройки или API меняют «abstract class interface» в реальном сервисе.