Интерфейсы, enum ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-07-24

Ассоциация, агрегация и композиция в Java

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

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

1. Отношения, основанные на иерархии классов: IS-A и HAS-A

Начнём с самой грубой классификации, основанной на иерархии классов, — это отношения IS-A и HAS-A.

Отношения между классами IS-A и HAS-A в Java

1.1. IS-A отношения («является»)

В ООП принцип IS-A основан на наследовании классов или реализации интерфейсов.

Например, если класс HeavyBox наследует Box, мы говорим, что HeavyBox является Box (HeavyBox IS-A 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:

Lorry IS-A Car

class Car {
    …
}

class Lorry extends Car {
    …
}

В этом случае Lorry IS-A Car.

Ещё пример — классы Person и Driver:

Driver IS-A Person

class Person {
    …
}

class Driver extends Person {
    …
}

Driver IS-A Person.

То же самое относится и к реализации интерфейсов. Если класс Transport реализует интерфейс Moveable, то они находятся в отношении Transport IS-A 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:

Car HAS-A Engine

class Car {
    Engine engine;
    …
}

class Engine {
    …
}

Мы говорим: Car HAS-A Engine.

Или Shop содержит массив Category:

Shop HAS-A 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 слишком общее, поэтому в проектировании выделяют три уточняющих вида связи — ассоциация, агрегация и композиция.

Ассоциация, агрегация и композиция в Java

2.1. Ассоциация

Начнём с ассоциации. В этих отношениях объекты двух классов могут ссылаться друг на друга. Ассоциация бывает односторонней (только один класс знает о другом) и двусторонней (оба хранят ссылки друг на друга).

Например, класс Person содержит переменную типа Car — значит, между Person и Car есть ассоциация:

Ассоциация между классами Person и Car

class Person {
    private String name;
    private Car car;
    …
}

class Car {
    …
}

У ассоциации есть кратность (множественность): один к одному (у человека один автомобиль), один ко многим (у магазина много категорий), многие ко многим (студенты и курсы). В коде кратность «ко многим» выражается коллекцией или массивом.

Агрегация и композиция — это частные случаи ассоциации. Агрегация — отношение, когда один объект является частью другого, но может существовать отдельно. Композиция — ещё более тесная связь: объект не просто входит в состав другого, но и не может принадлежать никому больше и не живёт без владельца. Разница станет очевидной на коде.

2.2. Агрегация

Объект класса Keyboard создаётся снаружи и передаётся в конструктор PC для установления связи. Если объект PC будет удалён, объект Keyboard может использоваться дальше — если, конечно, на него останется ссылка:

Агрегация в Java: 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:

Композиция в Java: PC и Keyboard

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. Когда что выбирать

  1. Наследование — только если подкласс является полноценной заменой родителя (принцип подстановки Барбары Лисков) и вы контролируете оба класса. Если вы наследуетесь только ради того, чтобы переиспользовать чужие методы, — берите композицию.
  2. Композиция — если вы собираете сложные объекты из простых «кирпичиков» и хотите менять поведение программы на лету, подставляя разные реализации.

Предостережение

Глубокие иерархии наследования (больше трёх уровней) делают код трудночитаемым и сложным для тестирования. Чем короче «родословная» классов, тем стабильнее система. Композиция плюс делегирование почти всегда дают более гибкую архитектуру, чем ещё один уровень 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 со связующей таблицей.

Презентация и видео на Patreon →

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

unknown Sep 28, 2022
Ассоциация – это когда один класс включает в себя другой класс в качестве одного из полей. Ассоциация описывается словом «имеет». Автомобиль имеет двигатель. Вполне естественно, что он не будет являться наследником двигателя (хотя такая архитектура тоже возможна в некоторых ситуациях). Выделяют два частных случая ассоциации: композицию и агрегацию.

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.