Отношения между классами в Java: наследование, композиция, агрегация

Отношения между классами в Java: наследование, композиция, агрегация Полезное

Отношения между классами в Java — это способы, которыми один класс связан с другим: наследует его тип, реализует интерфейс, хранит его объект внутри себя или обращается к нему на время выполнения метода. От выбора связи зависит, что произойдет с зависимыми объектами при изменении или удалении главного.

Все связи делятся на две большие группы. Отношение «is-a» («является») — когда один тип — частный случай другого: собака является животным. Отношение «has-a» («имеет») — когда один объект хранит другой как свою часть: у машины есть двигатель. Ниже разберем пять конкретных видов связи с кодом на Java и с тем, как их рисуют на диаграмме классов UML.

Сводная таблица видов связей

Связи отличаются силой: от жесткой (наследование, композиция) до самой слабой временной (зависимость). Ключевой практический признак — что стало бы с объектом B, если удалить объект A.

Связь Группа Ключевой признак Ключевое слово Java UML-нотация словами
Наследование is-a подкласс наследует тип и поведение суперкласса extends сплошная линия с полым треугольником к родителю
Реализация интерфейса is-a (контракт) класс обязуется реализовать методы интерфейса implements пунктирная линия с полым треугольником к интерфейсу
Композиция has-a (сильная) часть не живет без целого, целое ее и создает поле-объект, создан внутри сплошная линия с закрашенным ромбом у целого
Агрегация has-a (слабая) часть живет независимо, передается извне поле-объект, передан в конструктор сплошная линия с полым (незакрашенным) ромбом у целого
Ассоциация связь-ссылка объекты знают друг о друге, живут независимо поле-ссылка сплошная линия, иногда со стрелкой направления
Зависимость использование объект приходит только в параметр/локально, не хранится параметр метода пунктирная линия с открытой стрелкой

Наследование (is-a)

Наследование — механизм, при котором новый класс (подкласс) получает поля и методы существующего (суперкласса) и может их дополнять или переопределять. Читается как «является»: Dog является Animal.

class Animal {
    protected String name;

    Animal(String name) {
        this.name = name;
    }

    void eat() {
        System.out.println(name + " ест");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name); // вызов конструктора родителя
    }

    void bark() {
        System.out.println(name + " лает");
    }
}

Объект Dog умеет и то, что задано в Animal, и свое:

Dog dog = new Dog("Рекс");
dog.eat();  // Рекс ест   (метод достался от Animal)
dog.bark(); // Рекс лает  (собственный метод Dog)

Важное ограничение: в Java нет множественного наследования классов — у класса ровно один прямой суперкласс через extends. Если явного родителя нет, им становится Object. Наследовать поведение от нескольких источников можно только через интерфейсы (ниже).

Реализация интерфейса

Интерфейс — это контракт: набор сигнатур методов без реализации (в современной Java допустимы и default-методы с телом). Класс через implements обязуется предоставить реализацию. Класс может реализовать сразу несколько интерфейсов — так Java обходит запрет множественного наследования классов.

interface Swimmer {
    void swim(); // только сигнатура, без тела
}

class Duck extends Animal implements Swimmer {
    Duck(String name) {
        super(name);
    }

    @Override
    public void swim() {
        System.out.println(name + " плывет");
    }
}

Различаем: extends наследует готовую реализацию от одного класса; implements берет только обязательство реализовать методы, но интерфейсов может быть много. Duck одновременно является Animal (наследование) и является Swimmer (контракт).

Композиция и агрегация (has-a)

Обе связи описывают «целое и часть»: объект хранит другой объект как свое поле. Различаются они временем жизни части.

Различаем композицию и агрегацию: при композиции часть не существует отдельно от целого — целое ее создает и уничтожает вместе с собой. При агрегации часть существует независимо, ее передают в готовом виде, и после удаления целого она продолжает жить.

Композиция — двигатель создается внутри машины и без нее не существует:

class Engine {
    void start() {
        System.out.println("Двигатель запущен");
    }
}

class Car {
    // часть создается ВНУТРИ целого -> композиция
    private final Engine engine = new Engine();

    void start() {
        engine.start();
        System.out.println("Машина поехала");
    }
}

Здесь никто снаружи не передает Engine: он рождается вместе с Car и исчезает, когда Car становится недостижимым для сборщика мусора.

Агрегация — клуб состоит из студентов, но студенты заведены отдельно и живут сами по себе:

import java.util.List;

class Student {
    private final String name;

    Student(String name) {
        this.name = name;
    }

    String getName() {
        return name;
    }
}

class Club {
    // часть ПЕРЕДАЕТСЯ извне -> агрегация
    private final List<Student> members;

    Club(List<Student> members) {
        this.members = members;
    }

    int size() {
        return members.size();
    }
}

Студенты созданы раньше клуба и переживут его роспуск:

Student a = new Student("Аня");
Student b = new Student("Борис");
Club chess = new Club(List.of(a, b));
System.out.println(chess.size()); // 2
// клуб можно распустить, объекты a и b остаются валидными

Оговорка о границе модели: в Java нет деструкторов, и «уничтожение части» — это про логику владения и достижимость объекта для сборщика мусора, а не про ручное освобождение памяти. Композиция и агрегация — способ выразить намерение владения в коде, а не жесткое правило языка.

Ассоциация и зависимость

Эти две связи не про «часть целого», а про то, что один объект знает о другом или пользуется им.

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

Ассоциация — у курса есть закрепленный преподаватель, ссылка хранится в поле:

class Teacher {
    final String name;

    Teacher(String name) {
        this.name = name;
    }
}

class Course {
    private Teacher teacher; // хранимая ссылка -> ассоциация

    void assign(Teacher t) {
        this.teacher = t;
    }

    String teacherName() {
        return teacher == null ? "не назначен" : teacher.name;
    }
}

И Course, и Teacher создаются и живут независимо; они не часть друг друга, но связаны ссылкой. Агрегация — это, по сути, частный случай ассоциации со смыслом «целое-часть».

Зависимость — самая слабая связь: класс использует другой тип, но не держит его в себе.

class Report {
    String asText() {
        return "Отчет за сентябрь";
    }
}

class ReportPrinter {
    // объект приходит только в параметр и не сохраняется -> зависимость
    void print(Report report) {
        System.out.println(report.asText());
    }
}

ReportPrinter не имеет поля Report: как только print завершится, связь пропадет. На UML это самая тонкая линия — пунктир с открытой стрелкой.

Как выбрать связь: короткий алгоритм

  1. Один тип — частный случай другого («является»)? Наследование (extends) или реализация интерфейса (implements).
  2. Нужны поведения из нескольких источников? Интерфейсы, а не наследование.
  3. Один объект — неотъемлемая часть другого и создается внутри него? Композиция.
  4. Один объект хранит другой, но тот заведен отдельно и живет сам? Агрегация.
  5. Объекты просто ссылаются друг на друга как равные? Ассоциация.
  6. Чужой объект нужен лишь на время одного метода? Зависимость (параметр).

Практическое правило проектирования — «предпочитать композицию наследованию»: композиция гибче, потому что не привязывает класс к чужой иерархии типов и позволяет менять поведение через подмену вложенного объекта.

Выводы

  • Связи делятся на «is-a» (наследование, реализация интерфейса) и «has-a» (композиция, агрегация), плюс более слабые ассоциация и зависимость.
  • В Java нет множественного наследования классов (extends — один родитель), но интерфейсов через implements можно реализовать сколько угодно.
  • Композиция и агрегация различаются временем жизни части: в композиции часть создается внутри целого и не живет без него, в агрегации — передается извне и существует независимо.
  • Ассоциация хранит ссылку в поле (долгая связь), зависимость получает объект только в параметр метода (связь на время вызова).
  • При равных вариантах композицию предпочитают наследованию: меньше жесткой привязки к чужой иерархии.

Где применяется / связь с практикой

Выбор связи между классами — это ежедневная работа при проектировании любого Java-приложения: от модели предметной области до слоя сервисов. Ошибка на этом уровне (например, наследование там, где нужна композиция) позже оборачивается хрупким кодом, который трудно менять. Именно поэтому ООП-связи спрашивают на собеседованиях на позицию Java-разработчика.

Освойте тему на практике

Разобраться с ООП на практике и довести навык до уровня, когда выбор связи очевиден, помогает системное обучение с код-ревью. Основы синтаксиса, классов и объектных связей закладывает курс Java Basic от Otus. Прежде чем записываться, полезно послушать открытые уроки Otus — там разбирают реальные задачи и показывают формат занятий.

FAQ

Агрегация и композиция — это виды ассоциации?
Да. Обе описывают связь «целое-часть» и считаются частными случаями ассоциации. Композиция при этом строже: часть принадлежит только одному целому и разделяет его время жизни.

Можно ли одновременно наследовать класс и реализовать интерфейсы?
Да. Класс объявляют как class A extends B implements C, D: ровно один суперкласс через extends и любое число интерфейсов через implements.

Чем зависимость отличается от ассоциации на практике?
Признак — хранится ли ссылка. Если объект лежит в поле класса, это ассоциация; если приходит только в параметр метода и не сохраняется, это зависимость.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)