Вопрос 10 · Раздел 18

Что такое композиция и наследование

White-box reuse — видны внутренности родителя (наследование). Black-box reuse — только публичный интерфейс (композиция). Devirtualization — JIT заменяет виртуальный вызов на пря...

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Композиция и наследование — это два основных способа организации связей между классами в ООП. Наследование создаёт отношение “является” (IS-A): один класс становится частным случаем другого. Композиция создаёт отношение “имеет” (HAS-A): один объект содержит другой объект как часть.

Простая аналогия: Наследование — как получение черт от родителей (вы являетесь человеком). Композиция — как покупка телефона (вы имеете телефон, можете его заменить).

Пример наследования (IS-A):

public class Animal {
    public void eat() { System.out.println("Eating..."); }
}

public class Dog extends Animal {  // Dog IS-A Animal
    public void bark() { System.out.println("Woof!"); }
}

Пример композиции (HAS-A):

public class Engine {
    public void start() { System.out.println("Engine started"); }
}

public class Car {
    private final Engine engine;  // Car HAS-A Engine
    
    public Car(Engine engine) {
        this.engine = engine;
    }
    
    public void start() {
        engine.start();  // Делегирование
    }
}

Когда использовать:

  • Наследование: когда есть реальная иерархия типов (Dog IS-A Animal)
  • Композиция: в большинстве остальных случаев — это более гибкий подход

🟡 Middle Level

Как это работает

Наследование реализует связь “является” (IS-A) и использует механизм таблиц виртуальных методов (VTable) на уровне JVM. Наследник получает доступ к protected полям родителя — это “белый ящик” (White-box reuse).

Композиция реализует связь “имеет” (HAS-A) через вызовы методов другого объекта. Взаимодействие только через публичный интерфейс — это “чёрный ящик” (Black-box reuse).

White-box reuse — видны внутренности родителя (наследование). Black-box reuse — только публичный интерфейс (композиция). Devirtualization — JIT заменяет виртуальный вызов на прямой. Monomorphic = один тип вызова → inlining. Megamorphic = 3+ типа → поиск по таблице.

Практическое применение

Почему Effective Java говорит “Favor Composition over Inheritance”:

Критерий Наследование Композиция
Связь Статическая (compile-time) Динамическая (runtime)
Инкапсуляция Нарушается (видны protected поля) Сохраняется
Гибкость Нельзя сменить родителя Можно подменить компонент
Тестируемость Сложно мокать Легко подменить на mock
Хрупкость Изменение родителя ломает всех Изолированные изменения

Типичные ошибки

Ошибка Решение
Наследование ради переиспользования кода Использовать композицию + делегирование
Глубокая иерархия (5+ уровней) Рефакторинг в сторону композиции
ArrayListMySpecialList через наследование MySpecialList с полем List внутри

Альтернативы наследованию

  1. Strategy: вместо SmsNotification extends NotificationNotificationService с полем SenderStrategy
  2. Decorator: оборачивание объекта для добавления функций (логирование, кэширование)
  3. Delegation: класс реализует интерфейс, но вызывает методы делегата

Когда НЕ стоит использовать композицию

  • Чистые иерархии типов (исключения: IOExceptionSocketException)
  • Фреймворки с Template Method паттерном
  • Когда нужен полиморфизм и работа через общий базовый тип

Template Method vs Strategy: Template Method = вы контролируете алгоритм, подклассы — шаги. Strategy = клиент выбирает весь алгоритм целиком. Template Method — когда скелет алгоритма стабилен. Strategy — когда весь алгоритм может меняться.


🔴 Senior Level

Internal Implementation на уровне JVM

Наследование и VTable: При вызове виртуального метода JVM использует таблицу виртуальных методов (VTable). Каждый класс имеет свою VTable с указателями на реализации методов. При глубокой иерархии (5+ уровней) JIT-компилятору сложнее делать devirtualization и inlining.

// Наследование: invokevirtual, поиск в VTable
class Parent { void foo() {} }
class Child extends Parent { @Override void foo() {} }
// parent.foo() → invokevirtual → VTable lookup

// Композиция с final: JIT может заинлайнить
class Service {
    private final Delegate delegate;  // final → monomorphic call
    void execute() { delegate.run(); } // JIT inlines → прямой вызов
}

Архитектурные Trade-offs

Наследование:

  • ✅ Плюсы: Полиморфизм из коробки, переиспользование кода, Template Method
  • ❌ Минусы: Хрупкий базовый класс, утечка реализации, статическая связь

Композиция:

  • ✅ Плюсы: Динамическое поведение, тестируемость (легко мокать), слабая связность
  • ❌ Минусы: Больше boilerplate кода (делегирование), больше объектов в heap

Edge Cases

  1. Fragile Base Class Problem: изменение addAll в родителе, который вызывает add, ломает наследника, переопределившего оба методы
    • Решение: Композиция + Forwarding
    • Это деталь реализации HashSet: addAll вызывает add internally. Но полагаться на это нельзя — в другой реализации может быть иначе. Именно поэтому наследование от классов, не предназначенных для этого, хрупко.
  2. Shared State при композиции:多个 делегатов работают с общими данными
    • Решение: Явная передача контекста или State Object
  3. Circular Delegation: A → B → A → StackOverflowError
    • Решение: Архитектурный анализ, ArchUnit правила

Производительность

Метрика Наследование Композиция
Object size Все поля родителей в одном объекте Отдельные объекты + заголовки
Method call invokevirtual (VTable lookup) invokevirtual + possible inlining
JIT optimization Сложнее при deep hierarchy Проще с final полями
Memory ~16 byte header + все поля ~16 bytes × N объектов + ссылки
  • Inlining: JIT легко инлайнит final делегатов → оверхед ~0
  • VTable: При глубине 5+ уровней megamorphic calls → деоптимизация

Thread Safety

  • Наследование: synchronized на методе родителя блокирует весь объект
  • Композиция: Можно использовать разные lock’и для разных делегатов → finer-grained concurrency

Production Experience

Рефакторинг в enterprise проекте: NotificationService на 2000 строк с наследниками EmailNotification, SmsNotification, PushNotification. Проблема: добавление новой стратегии требовало изменения базового класса.

Рефакторинг: выделили NotificationStrategy интерфейс, каждый тип — отдельная реализация. NotificationService стал координатором. Результат: новые типы добавляются без изменения существующего кода (OCP).

Monitoring

ArchUnit правила:

@ArchTest
static void no_deep_inheritance = classes()
    .should().notHaveSimpleNameMatching(".*Impl.*")
    .andShould().haveLessThanNAncestors(3);

SonarQube: Depth of Inheritance Hierarchy > 5 → Code Smell

Best Practices for Highload

  • final делегаты для лучшего inlining
  • Package-private для внутренних компонентов
  • Sealed classes (Java 17+) для контролируемого наследования

Sealed classes доступны с Java 17. Для Java 8/11 используйте final классы и документируйте, какие классы предназначены для наследования.

  • Composition over Inheritance как default выбор

🎯 Шпаргалка для интервью

Обязательно знать:

  • Наследование = IS-A (статическая связь, white-box reuse), Композиция = HAS-A (динамическая, black-box)
  • “Favor Composition over Inheritance” — Effective Java, Josh Bloch
  • Композиция сохраняет инкапсуляцию, наследование нарушает (видны protected поля)
  • JIT легко инлайнит final делегатов → оверхед композиции ~0
  • Наследование: хрупкий базовый класс, утечка реализации, статическая связь
  • Fragile Base Class: изменение addAll в родителе ломает наследника, переопределившего add
  • Sealed classes (Java 17+) для контролируемого наследования

Частые уточняющие вопросы:

  • Почему композиция лучше? — Динамическая замена компонентов, тестируемость (легко мокать), слабая связность
  • Когда наследование допустимо? — Истинное IS-A, фреймворки с Template Method, общие инфраструктурные поля (BaseEntity)
  • Что такое devirtualization? — JIT заменяет виртуальный вызов на прямой, когда знает точный тип
  • VTable overhead при наследовании? — 5+ уровней → megamorphic calls → деоптимизация JIT

Красные флаги (НЕ говорить):

  • “Наследование — лучший способ переиспользовать код” (главная причина хрупкости)
  • “Глубокая иерархия — признак хорошего дизайна” (5+ уровней — code smell)
  • “Композиция всегда лучше” (есть legit случаи наследования: Template Method, IS-A)

Связанные темы:

  • [[11. В каких случаях лучше использовать композицию вместо наследования]]
  • [[12. Что такое делегирование в ООП]]
  • [[5. Что такое принцип Liskov Substitution]]
  • [[6. Приведите пример нарушения принципа Liskov Substitution]]