Ассоциация, агрегация и композиция в Java
Ассоциация, агрегация и композиция — это три вида связей между классами в Java, описывающих отношение HAS-A («имеет»). Ассоциация — любая связь между объектами; агрегация — связь «часть—целое», где часть живёт независимо от целого; композиция — жёсткая связь «часть—целое», где часть создаётся и умирает вместе с целым. Отдельно стоит отношение IS-A («является»), которое реализуется наследованием и имплементацией интерфейсов.
Большинство классов приложения так или иначе связаны между собой. В этом уроке разберём две классификации отношений между классами: базовую (IS-A / HAS-A) и более детальную (ассоциация, агрегация, композиция), а также правило «композиция вместо наследования».
1. Отношения, основанные на иерархии классов: IS-A и HAS-A
Начнём с самой грубой классификации, основанной на иерархии классов, — это отношения IS-A и HAS-A.

1.1. IS-A отношения («является»)
В ООП принцип IS-A основан на наследовании классов или реализации интерфейсов.
Например, если класс HeavyBox наследует Box, мы говорим, что HeavyBox является Box (HeavyBox IS-A Box):

class Box {
private double width;
private double height;
private double depth;
…
}
class HeavyBox extends Box {
private int weight;
…
} Или другой пример — класс Lorry (грузовик) расширяет класс Car:

class Car {
…
}
class Lorry extends Car {
…
} В этом случае Lorry IS-A Car.
Ещё пример — классы Person и Driver:

class Person {
…
}
class Driver extends Person {
…
} Driver IS-A Person.
То же самое относится и к реализации интерфейсов. Если класс Transport реализует интерфейс Moveable, то они находятся в отношении Transport IS-A Moveable:

interface Moveable {
…
}
class Transport implements Moveable {
…
} Проверить IS-A в коде можно оператором instanceof: выражение transport instanceof Moveable вернёт true, потому что Transport IS-A Moveable.
1.2. HAS-A отношения («имеет»)
HAS-A отношения основаны на использовании: один класс хранит ссылку на объект другого класса в поле.
Например, класс Car содержит переменную типа Engine:

class Car {
Engine engine;
…
}
class Engine {
…
} Мы говорим: Car HAS-A Engine.
Или Shop содержит массив Category:

class Shop {
Category[] categories;
…
}
class Category {
…
} Это тоже отношение HAS-A: Shop HAS-A Category.
Как быстро определить вид связи
Подставьте классы в фразу. Если корректно звучит «A является B» — это IS-A, наследование. Если «A имеет B» — это HAS-A, то есть ассоциация, агрегация или композиция. Грузовик является машиной, но машина имеет двигатель — наследовать Car от Engine нельзя.
2. Более детальная классификация: ассоциация, агрегация и композиция
Отношение HAS-A слишком общее, поэтому в проектировании выделяют три уточняющих вида связи — ассоциация, агрегация и композиция.

2.1. Ассоциация
Начнём с ассоциации. В этих отношениях объекты двух классов могут ссылаться друг на друга. Ассоциация бывает односторонней (только один класс знает о другом) и двусторонней (оба хранят ссылки друг на друга).
Например, класс Person содержит переменную типа Car — значит, между Person и Car есть ассоциация:

class Person {
private String name;
private Car car;
…
}
class Car {
…
} У ассоциации есть кратность (множественность): один к одному (у человека один автомобиль), один ко многим (у магазина много категорий), многие ко многим (студенты и курсы). В коде кратность «ко многим» выражается коллекцией или массивом.
Агрегация и композиция — это частные случаи ассоциации. Агрегация — отношение, когда один объект является частью другого, но может существовать отдельно. Композиция — ещё более тесная связь: объект не просто входит в состав другого, но и не может принадлежать никому больше и не живёт без владельца. Разница станет очевидной на коде.
2.2. Агрегация
Объект класса Keyboard создаётся снаружи и передаётся в конструктор PC для установления связи. Если объект PC будет удалён, объект Keyboard может использоваться дальше — если, конечно, на него останется ссылка:

public class PC {
private Keyboard keyboard;
public PC(Keyboard keyboard) { // объект приходит извне
this.keyboard = keyboard;
}
} Keyboard keyboard = new Keyboard();
PC pc = new PC(keyboard);
pc = null; // компьютер больше не нужен
keyboard.press('A'); // клавиатура жива и работает Житейский пример агрегации: отдел и сотрудники. Отдел расформировали — сотрудники никуда не исчезли, их переведут в другой отдел.
public class Department {
private final List<Employee> employees;
public Department(List<Employee> employees) {
this.employees = employees; // сотрудники существуют независимо
}
} 2.3. Композиция
Теперь посмотрим на реализацию композиции. Объект класса Keyboard создаётся внутри конструктора, что означает более тесную связь между объектами. Такой Keyboard не задумывался как самостоятельный объект и не переживает создавший его PC:

public class PC {
private final Keyboard keyboard;
public PC() {
this.keyboard = new Keyboard(); // объект рождается вместе с PC
}
} Классический пример композиции — дом и комнаты: снесли дом — комнат больше нет.
public class House {
private final List<Room> rooms = new ArrayList<>();
public House(int roomCount) {
for (int i = 0; i < roomCount; i++) {
rooms.add(new Room(i)); // комнаты создаёт сам дом
}
}
public List<Room> getRooms() {
return List.copyOf(rooms); // наружу отдаём копию, а не сам список
}
} Важно
В Java композиция — это соглашение проектирования, а не гарантия языка. Если геттер вернёт наружу ссылку на внутренний объект, тот переживёт владельца и связь фактически превратится в агрегацию. Поэтому части в композиции держат в private final-полях и отдают только копии или неизменяемые представления.
3. Агрегация и композиция: в чём разница
Коротко: агрегация — «часть может жить без целого и принадлежать нескольким целым», композиция — «часть создаётся целым, принадлежит только ему и умирает вместе с ним». Ассоциация — зонтичный термин: и агрегация, и композиция являются её разновидностями.
| Критерий | Ассоциация | Агрегация | Композиция |
|---|---|---|---|
| Смысл связи | «знает о», использует | «часть—целое», слабая | «часть—целое», сильная |
| Кто создаёт часть | Не важно | Внешний код, передаётся в конструктор или сеттер | Само целое, внутри конструктора |
| Жизненный цикл | Независимые | Часть переживает целое | Часть умирает вместе с целым |
| Может ли часть принадлежать нескольким целым | Да | Да | Нет |
| Обозначение в UML | Простая линия (со стрелкой при односторонней связи) | Незакрашенный (белый) ромб у целого | Закрашенный (чёрный) ромб у целого |
| Пример | Person — Car | Department — Employee, PC — Keyboard | House — Room, Human — Heart |
3.1. И ещё одна связь — зависимость (dependency)
На собеседовании часто добавляют четвёртый вариант — зависимость. Это самая слабая связь: класс не хранит объект в поле, а лишь получает его как параметр метода или создаёт локально:
class ReportService {
// Нет поля типа Printer - только зависимость от него
void print(Printer printer) {
printer.print(buildReport());
}
} По силе связи виды отношений выстраиваются так: зависимость → ассоциация → агрегация → композиция → наследование.
4. Композиция вместо наследования (Composition over Inheritance)
В объектно-ориентированном проектировании есть важное правило: отдавайте предпочтение композиции перед наследованием. Оно сформулировано ещё в книге «Приёмы объектно-ориентированного проектирования» (банда четырёх) и повторено Джошуа Блохом в Effective Java.
4.1. Чем опасно избыточное наследование
Наследование — мощный инструмент, но при чрезмерном использовании оно создаёт проблемы:
- Хрупкость кода. Изменение логики базового класса может неожиданно «сломать» десятки наследников.
- Нарушение инкапсуляции. Подкласс часто зависит от деталей реализации родителя, а не только от его контракта.
- Жёсткая иерархия. Родителя нельзя поменять во время выполнения программы: связь фиксируется на этапе компиляции.
- Нет множественного наследования. В Java нельзя наследоваться от двух классов сразу. Если нужны возможности и «Принтера», и «Сканера», создать «МФУ» через наследование не получится — а через композицию легко.
4.2. Пример: сотрудник и его роль
Представим, что мы создаём систему управления персоналом.
Слабый подход (наследование):
class Employee {}
class Driver extends Employee {} // а если водитель станет менеджером?
// придётся создавать новый объект Хороший подход (композиция): вместо того чтобы говорить, что сотрудник является водителем, скажем, что у сотрудника есть роль.
class Role {}
class DriverRole extends Role {}
class ManagerRole extends Role {}
class Employee {
private Role role; // композиция: сотрудник "имеет" роль
public void setRole(Role role) {
this.role = role; // роль можно сменить в любой момент
}
} 4.3. Когда что выбирать
- Наследование — только если подкласс является полноценной заменой родителя (принцип подстановки Барбары Лисков) и вы контролируете оба класса. Если вы наследуетесь только ради того, чтобы переиспользовать чужие методы, — берите композицию.
- Композиция — если вы собираете сложные объекты из простых «кирпичиков» и хотите менять поведение программы на лету, подставляя разные реализации.
Предостережение
Глубокие иерархии наследования (больше трёх уровней) делают код трудночитаемым и сложным для тестирования. Чем короче «родословная» классов, тем стабильнее система. Композиция плюс делегирование почти всегда дают более гибкую архитектуру, чем ещё один уровень extends.
5. На чём чаще всего ошибаются
- Считают, что любое поле-объект — это композиция. Само по себе наличие поля означает лишь ассоциацию. Композиция определяется тем, кто управляет жизненным циклом части.
- Ломают композицию геттером.
return rooms;вместоreturn List.copyOf(rooms);отдаёт наружу внутреннее состояние — и часть начинает жить своей жизнью. - Двусторонняя ассоциация без синхронизации. Если
Personссылается наCar, аCar— наPerson, легко получить рассогласованные данные, бесконечную рекурсию вtoString()/equals()и утечку памяти в кэше. - Наследуются ради переиспользования кода. Классический антипример:
class Stack extends ArrayList— стек получаетadd(int, E)и перестаёт быть стеком. Правильнее держать список полем. - Путают IS-A с HAS-A. «Автомобиль — это двигатель» звучит неверно, значит,
extendsздесь неуместен.
6. Кратко
- IS-A — наследование или реализация интерфейса (
extends,implements). - HAS-A — объект хранится в поле другого объекта.
- Ассоциация — общая связь между объектами, односторонняя или двусторонняя.
- Агрегация — часть приходит извне и живёт без целого (белый ромб в UML).
- Композиция — часть создаётся целым и умирает вместе с ним (чёрный ромб в UML).
- Composition over inheritance — гибкость и слабая связанность вместо жёсткой иерархии.
Часто задаваемые вопросы
Агрегация и композиция — это IS-A или HAS-A?
Обе относятся к HAS-A: объект хранит ссылку на другой объект в поле. IS-A появляется только при наследовании класса или реализации интерфейса. То есть агрегация и композиция — это уточнения HAS-A по силе связи и по жизненному циклу части.
Чем ассоциация отличается от зависимости?
При ассоциации объект хранится в поле класса, и связь существует всё время жизни объекта. При зависимости объект приходит только как параметр метода, локальная переменная или возвращаемое значение — связь существует лишь на время вызова. Зависимость — самая слабая из связей.
Можно ли случайно нарушить композицию в Java?
Да. Java не запрещает вернуть ссылку на внутренний объект: достаточно геттера, отдающего сам список или сам объект, и часть переживёт владельца, превратив композицию в агрегацию. Чтобы этого не случилось, храните части в private final полях и возвращайте копии, неизменяемые представления или отдельные значения.
Как реализовать ассоциацию многие ко многим?
В обоих классах заводят коллекцию ссылок на другой класс: у студента список курсов, у курса список студентов. Добавление связи нужно выполнять в одном методе, который обновляет обе стороны, иначе данные рассогласуются. В JPA такая связь описывается аннотацией ManyToMany со связующей таблицей.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии