Наследование и композиция это два способа переиспользовать код. При наследовании класс расширяет другой через extends и получает все его поля и методы: отношение «является» (IS-A). При композиции класс хранит другой объект в поле и делегирует ему работу: отношение «имеет» (HAS-A). Общая рекомендация: начинать с композиции, а наследование брать только для стабильной иерархии типов, где наследник действительно является частным случаем родителя. Ниже на примере системы учета сотрудников видно, почему.![Поединок двух бойцов в клетке: наследование против композиции]()
Кратко
- Наследование (extends) жестко связывает классы: наследник получает все методы родителя, даже ненужные, и правка родителя ломает всех потомков.
- Композиция собирает объект из частей: поведение выносится в интерфейсы (SalaryCalculator, TimeTracker), а класс просто делегирует им вызовы.
- Композиция обходит запрет множественного наследования: комбинаций поведения может быть сколько угодно, и менять их можно прямо во время работы программы.
- Признак, что нужно наследование: честное «является» и стабильная иерархия. Признак композиции: «имеет», сменное поведение, несколько источников функциональности.
- Классический антипример из JDK: Stack наследует Vector и вместе с ним получает методы, которые ломают дисциплину стека.
- Золотое правило «предпочитай композицию наследованию» не запрещает наследование, а говорит, с чего начинать выбор.

Наследование: мощный, но опасный инструмент
Наследование это когда один класс расширяет другой, получая все его поля и методы. Звучит удобно, правда? Но есть подводные камни.Пример: система управления сотрудниками
Представь, что ты разрабатываешь систему учета сотрудников для компании:public class Employee {
private String name;
private String id;
private double baseSalary;
public Employee(String name, String id, double baseSalary) {
this.name = name;
this.id = id;
this.baseSalary = baseSalary;
}
public double calculateSalary() {
return baseSalary;
}
public void clockIn() {
System.out.println(name + " пришел на работу в " + getCurrentTime());
}
public void clockOut() {
System.out.println(name + " ушел с работы в " + getCurrentTime());
}
private String getCurrentTime() {
return java.time.LocalTime.now().toString();
}
// Геттеры опущены для краткости
}
Теперь нужно добавить менеджера с бонусами:
public class Manager extends Employee {
private double bonus;
public Manager(String name, String id, double baseSalary, double bonus) {
super(name, id, baseSalary);
this.bonus = bonus;
}
@Override
public double calculateSalary() {
return super.calculateSalary() + bonus;
}
}
Пока все выглядит отлично! Но подожди... 🤔Проблема №1: Хрупкость базового класса
Через полгода HR-отдел просит добавить фрилансеров. Фрилансеры тоже сотрудники, но им не нужно отмечать приход и уход на работу, они работают удаленно:public class Freelancer extends Employee {
private double hourlyRate;
private int hoursWorked;
public Freelancer(String name, String id, double hourlyRate, int hoursWorked) {
super(name, id, 0); // Базовая зарплата = 0
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
@Override
public double calculateSalary() {
return hourlyRate * hoursWorked;
}
// Но что делать с clockIn() и clockOut()?
// Они унаследовались, но для фрилансера бессмысленны!
}
Видишь проблему? Фрилансер унаследовал методы clockIn() и clockOut(), которые ему не нужны. Можно, конечно, переопределить их как пустые, но это костыль.Проблема №2: Жесткая связанность
Теперь представь, что компания решила изменить систему учета рабочего времени. Вместо простого clockIn/clockOut нужна интеграция с биометрическими терминалами. Придется менять базовый класс Employee, а это затронет ВСЕ дочерние классы, даже те, которым эти изменения не нужны. Твой код стал хрупким: изменения в родительском классе могут сломать наследников.Проблема №3: Невозможность множественного наследования
А что если сотрудник одновременно и программист, и менеджер проекта? В Java нельзя унаследоваться от двух классов:// Такое не работает в Java!
public class TechLead extends Programmer, Manager {
// Ошибка компиляции
}Композиция: гибкое решение
Композиция это когда объект содержит другие объекты как свои части. Вместо отношения «является» (IS-A) мы используем «имеет» (HAS-A). Давай перепишем нашу систему:Шаг 1: Выделяем поведение в отдельные классы
// Интерфейс для расчета зарплаты
public interface SalaryCalculator {
double calculate();
}
// Фиксированная зарплата
public class FixedSalary implements SalaryCalculator {
private final double amount;
public FixedSalary(double amount) {
this.amount = amount;
}
@Override
public double calculate() {
return amount;
}
}
// Почасовая оплата
public class HourlySalary implements SalaryCalculator {
private final double hourlyRate;
private int hoursWorked;
public HourlySalary(double hourlyRate) {
this.hourlyRate = hourlyRate;
}
public void addHours(int hours) {
this.hoursWorked += hours;
}
@Override
public double calculate() {
return hourlyRate * hoursWorked;
}
}
// Зарплата с бонусом: обертка над другим калькулятором
public class SalaryWithBonus implements SalaryCalculator {
private final SalaryCalculator baseSalary;
private final double bonus;
public SalaryWithBonus(SalaryCalculator baseSalary, double bonus) {
this.baseSalary = baseSalary;
this.bonus = bonus;
}
@Override
public double calculate() {
return baseSalary.calculate() + bonus;
}
}Обрати внимание на SalaryWithBonus: он принимает другой SalaryCalculator и добавляет к его результату бонус. Это паттерн Декоратор в чистом виде, и работает он именно за счет композиции.Шаг 2: Система учета времени
// Интерфейс для учета времени
public interface TimeTracker {
void clockIn();
void clockOut();
}
// Обычный учет времени
public class OfficeTimeTracker implements TimeTracker {
private final String employeeName;
public OfficeTimeTracker(String employeeName) {
this.employeeName = employeeName;
}
@Override
public void clockIn() {
System.out.println("Вход в офис: " + employeeName + ", " + getCurrentTime());
}
@Override
public void clockOut() {
System.out.println("Выход из офиса: " + employeeName + ", " + getCurrentTime());
}
private String getCurrentTime() {
return java.time.LocalTime.now().toString();
}
}
// Пустой трекер для удаленных сотрудников
public class NoTimeTracker implements TimeTracker {
@Override
public void clockIn() {
// Для фрилансеров не нужно
}
@Override
public void clockOut() {
// Для фрилансеров не нужно
}
}Шаг 3: Новый класс сотрудника
public class Employee {
private final String name;
private final String id;
private final SalaryCalculator salaryCalculator;
private final TimeTracker timeTracker;
public Employee(String name, String id,
SalaryCalculator salaryCalculator,
TimeTracker timeTracker) {
this.name = name;
this.id = id;
this.salaryCalculator = salaryCalculator;
this.timeTracker = timeTracker;
}
public double getSalary() {
return salaryCalculator.calculate();
}
public void clockIn() {
timeTracker.clockIn();
}
public void clockOut() {
timeTracker.clockOut();
}
public String getName() {
return name;
}
public String getId() {
return id;
}
}Шаг 4: Создаем разных сотрудников
public class CompanyDemo {
public static void main(String[] args) {
// Обычный сотрудник с фиксированной зарплатой
Employee john = new Employee(
"Джон",
"EMP001",
new FixedSalary(5000),
new OfficeTimeTracker("Джон")
);
// Менеджер с бонусом
Employee sarah = new Employee(
"Сара",
"MNG001",
new SalaryWithBonus(new FixedSalary(7000), 1500),
new OfficeTimeTracker("Сара")
);
// Фрилансер с почасовой оплатой
HourlySalary freelancerSalary = new HourlySalary(100);
freelancerSalary.addHours(120);
Employee alex = new Employee(
"Алекс",
"FRL001",
freelancerSalary,
new NoTimeTracker()
);
// Используем
john.clockIn();
System.out.println("Зарплата Джона: " + john.getSalary() + " usd.");
sarah.clockIn();
System.out.println("Зарплата Сары: " + sarah.getSalary() + " usd.");
alex.clockIn(); // Ничего не выведет
System.out.println("Зарплата Алекса: " + alex.getSalary() + " usd.");
}
}
Результат выполнения:
Вход в офис: Джон, 09:15:23.456
Зарплата Джона: 5000.0 usd.
Вход в офис: Сара, 09:16:10.789
Зарплата Сары: 8500.0 usd.
Зарплата Алекса: 12000.0 usd.
Преимущества композиции
1. Гибкость конфигурации
Можешь создавать любые комбинации поведения:// Сотрудник с фиксированной зарплатой + бонус + офисный трекер
Employee mixedEmployee = new Employee(
"Мария",
"EMP002",
new SalaryWithBonus(new FixedSalary(60000), 10000),
new OfficeTimeTracker("Мария")
);2. Легко менять поведение в рантайме
Хочешь изменить систему учета времени? Просто передай другую реализацию TimeTracker. Обрати внимание: ради сеттера поле timeTracker придется объявить без final, в отличие от варианта из шага 3.public class Employee {
private TimeTracker timeTracker;
public void setTimeTracker(TimeTracker tracker) {
this.timeTracker = tracker;
}
}
// Сотрудник перешел на удаленку
employee.setTimeTracker(new NoTimeTracker());3. Независимость компонентов
Изменения в TimeTracker не влияют на SalaryCalculator. Компоненты слабо связаны.4. Можно комбинировать множество поведений
Помнишь проблему с TechLead? Теперь это не проблема:// Создаем уникальную комбинацию поведения
Employee techLead = new Employee(
"Игорь",
"TL001",
new SalaryWithBonus(new HourlySalary(2000), 30000),
new OfficeTimeTracker("Игорь")
);Реальный пример из Java API
Знаешь, в стандартной библиотеке Java есть класс Stack, который наследуется от Vector:public class Stack<E> extends Vector<E> {
// ...
}
Это был плохой дизайн! Stack получил все методы Vector, включая те, которые нарушают концепцию стека (например, add(index, element)).
Класс никуда не делся и работает до сих пор, но в его документации прямо написано: более полный и последовательный набор операций стека дает интерфейс Deque, и использовать надо его реализации, например ArrayDeque. А свой стек поверх ArrayDeque собирают уже через композицию:
public class Stack<E> {
private final Deque<E> elements = new ArrayDeque<>();
public void push(E item) {
elements.addFirst(item);
}
public E pop() {
return elements.removeFirst();
}
}Когда использовать наследование, а когда композицию?
Используй наследование, если:
- ✅ Есть четкая иерархия типов с отношением «является» (IS-A)
- ✅ Дочерний класс действительно должен наследовать ВСЕ поведение родителя
- ✅ Иерархия стабильна и вряд ли изменится
- ✅ Хочешь использовать полиморфизм через общий интерфейс
Используй композицию, если:
- ✅ Нужна гибкость в комбинировании поведения
- ✅ Отношение «имеет» (HAS-A): Car имеет Engine
- ✅ Поведение может меняться в рантайме
- ✅ Хочешь избежать хрупкости базового класса
- ✅ Нужно комбинировать несколько источников функциональности
Золотое правило
«Предпочитай композицию наследованию» это один из ключевых принципов ООП, сформулированный еще в книге «Приемы объектно-ориентированного проектирования» банды четырех. Это не значит, что наследование плохое, просто композиция дает больше гибкости в большинстве случаев. Начни с композиции, и только если уверен, что нужна жесткая иерархия типов, используй наследование.Наследование и композиция: сравнение
Соберем различия в одну таблицу:| Критерий | Наследование | Композиция |
|---|---|---|
| Отношение | «является» (IS-A): Manager это Employee | «имеет» (HAS-A): Employee имеет SalaryCalculator |
| Связь классов | жесткая, задана на этапе компиляции | слабая, через интерфейс |
| Смена поведения в рантайме | невозможна: тип объекта уже не изменить | достаточно передать другую реализацию |
| Несколько источников поведения | нельзя: у класса только один родитель | сколько угодно полей с разными ролями |
| Что достается наследнику или владельцу | все методы родителя, включая ненужные | только то, что класс сам решил делегировать |
| Риск | хрупкий базовый класс: правка родителя ломает потомков | больше классов и объектов, нужен продуманный набор интерфейсов |
| Типичный случай | стабильная иерархия типов: Animal, Dog, Cat | сменные стратегии поведения, обертки, декораторы |
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ