Что такое композиция и наследование
White-box reuse — видны внутренности родителя (наследование). Black-box reuse — только публичный интерфейс (композиция). Devirtualization — JIT заменяет виртуальный вызов на пря...
🟢 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+ уровней) | Рефакторинг в сторону композиции |
ArrayList → MySpecialList через наследование |
MySpecialList с полем List внутри |
Альтернативы наследованию
- Strategy: вместо
SmsNotification extends Notification→NotificationServiceс полемSenderStrategy - Decorator: оборачивание объекта для добавления функций (логирование, кэширование)
- Delegation: класс реализует интерфейс, но вызывает методы делегата
Когда НЕ стоит использовать композицию
- Чистые иерархии типов (исключения:
IOException→SocketException) - Фреймворки с 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
- Fragile Base Class Problem: изменение
addAllв родителе, который вызываетadd, ломает наследника, переопределившего оба методы- Решение: Композиция + Forwarding
- Это деталь реализации HashSet: addAll вызывает add internally. Но полагаться на это нельзя — в другой реализации может быть иначе. Именно поэтому наследование от классов, не предназначенных для этого, хрупко.
- Shared State при композиции:多个 делегатов работают с общими данными
- Решение: Явная передача контекста или State Object
- 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]]