JavaJuniorTechnical

Объясните различия между абстрактными классами и интерфейсами в Java 17+.

Абстрактный класс даёт частичную реализацию и состояние; интерфейс — контракт без состояния. Класс может реализовывать много интерфейсов, но наследовать только один класс. С Java 8+ интерфейсы поддерживают default/static-методы.

Ключевое отличие

Абстрактный класс предназначен для is-a-иерархии с общим состоянием и частичной реализацией. Интерфейс описывает роль или способность — контракт без полей экземпляра. Класс наследует ровно один абстрактный класс, но реализует произвольное число интерфейсов.

Что разрешено где

Abstract classInterface (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» в реальном сервисе.

Sources

Related topics