Skip to content
IRC-CodingIRC-Coding
OOPKlassenbeziehungenассоциацияагрегациякомпозициянаследованиеGenericsстатические членыэкзамен IHK

OOP: ассоциация, агрегация, композиция и наследование

Отношения между классами в ООП: ассоциация, агрегация, композиция, наследование, статические члены и Generics для экзамена IHK.

S

schutzgeist

12 min read
OOP: ассоциация, агрегация, композиция и наследование

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

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

1. Структура класса (Внутреннее устройство)

Класс — это шаблон. Он состоит из нескольких определённых компонентов:

1. Имя класса

Уникальное имя класса (например, Kunde, Bankkonto). По соглашению начинается с прописной буквы.

2. Атрибуты (поля данных)

Переменные внутри класса, которые описывают состояние будущего объекта.

  • Каждый атрибут имеет имя (например, kontostand) и тип данных (например, double, String)
  • Обычно объявляются с модификатором private (см. инкапсуляция)

3. Конструктор

Специальный метод, который вызывается автоматически при создании нового объекта класса с помощью new.

Назначение: инициализирует новые объекты, то есть присваивает атрибутам начальные значения.

  • Конструктор имеет такое же имя, как класс и не имеет типа возврата (даже не void)
  • Может быть несколько конструкторов (перегрузка), например конструктор без параметров и конструктор, инициализирующий все атрибуты
// Пример конструктора
public class Bankkonto {
    private double kontostand;
    private String inhaber;
    
    // Конструктор
    public Bankkonto(String inhaberName, double startGuthaben) {
        inhaber = inhaberName;
        kontostand = startGuthaben;
    }
    
    // Конструктор без параметров
    public Bankkonto() {
        this("Unbekannt", 0.0);
    }
    
    // Перегруженный конструктор
    public Bankkonto(String inhaberName) {
        this(inhaberName, 0.0);
    }
}

// Создание объектов:
Bankkonto meinKonto = new Bankkonto("Max Mustermann", 1000.0);
Bankkonto leeresKonto = new Bankkonto();

4. Методы (функции-члены)

Определяют поведение объектов. Они работают с атрибутами.

  • Getter/Setter: публичные методы для контролируемого чтения и записи private атрибутов
  • Бизнес-методы: реализуют основную функциональность (например, geldEinzahlen(double betrag), geldAbheben(double betrag))

5. Модификаторы видимости (Access Modifiers)

Определяют, откуда можно обращаться к компонентам класса. Важны для инкапсуляции.

  • private: видны только внутри своего класса
  • protected: видны внутри своего класса и его подклассов (наследование)
  • public: видны отовсюду
  • (package-private/default): видны только внутри одного пакета/пространства имён

2. Отношения между классами: как классы связаны друг с другом

Классы редко существуют изолированно. Они работают вместе. Эти отношения моделируются в диаграммах классов UML.

a) Ассоциация

Описание: самое общее отношение. Описывает семантическую связь между двумя независимыми классами. Один класс “знает о” другом.

Представление в UML: сплошная линия.

Пример: Professor преподаёт Student. Профессор и студент существуют независимо друг от друга. Отношение часто двусторонне (оба “знают друг о друге”).

Реализация: обычно через атрибуты-ссылки.

// Пример ассоциации
public class Professor {
    private String name;
    private String fachbereich;
    private List<Student> studenten; // Professor АССОЦИИРОВАН со Student
    
    public Professor(String name, String fachbereich) {
        this.name = name;
        this.fachbereich = fachbereich;
        this.studenten = new ArrayList<>();
    }
    
    public void addStudent(Student student) {
        studenten.add(student);
        student.addProfessor(this); // Двусторонняя связь
    }
    
    public void unterrichten() {
        System.out.println("Professor " + name + " unterrichtet " + studenten.size() + " Studenten");
    }
    
    // Getter
    public String getName() { return name; }
    public List<Student> getStudenten() { return new ArrayList<>(studenten); }
}

public class Student {
    private String name;
    private int matrikelNr;
    private List<Professor> professoren; // Student АССОЦИИРОВАН с Professor
    
    public Student(String name, int matrikelNr) {
        this.name = name;
        this.matrikelNr = matrikelNr;
        this.professoren = new ArrayList<>();
    }
    
    public void addProfessor(Professor professor) {
        professoren.add(professor);
    }
    
    public void lerne() {
        System.out.println("Student " + name + " lernt bei " + professoren.size() + " Professoren");
    }
    
    // Getter
    public String getName() { return name; }
    public List<Professor> getProfessoren() { return new ArrayList<>(professoren); }
}

b) Агрегация

Описание: более конкретная форма ассоциации. Описывает отношение “является частью” или “имеет”, при котором части могут существовать и без целого. Это слабая связь.

Представление в UML: сплошная линия с пустым ромбом на стороне целого.

Пример: Abteilung имеет Mitarbeiter. Если отдел закрывается, сотрудники не исчезают, их просто переводят в другой отдел. Часть (сотрудник) существует независимо от целого (отдела).

Реализация: как ассоциация, но жизненные циклы объектов не связаны.

// Пример агрегации
public class Abteilung {
    private String name;
    private String standort;
    private List<Mitarbeiter> mitarbeiter; // Abteilung HAT Mitarbeiter (агрегация)
    
    public Abteilung(String name, String standort) {
        this.name = name;
        this.standort = standort;
        this.mitarbeiter = new ArrayList<>();
    }
    
    public void addMitarbeiter(Mitarbeiter mitarbeiter) {
        this.mitarbeiter.add(mitarbeiter);
        mitarbeiter.setAbteilung(this);
    }
    
    public void removeMitarbeiter(Mitarbeiter mitarbeiter) {
        this.mitarbeiter.remove(mitarbeiter);
        mitarbeiter.setAbteilung(null); // Сотрудник продолжает существовать!
    }
    
    public void aufloesen() {
        // Сотрудники не уничтожаются, только удаляются из отдела
        for (Mitarbeiter m : mitarbeiter) {
            m.setAbteilung(null);
        }
        mitarbeiter.clear();
        System.out.println("Abteilung " + name + " aufgelöst, Mitarbeiter existieren weiter");
    }
    
    // Getter
    public String getName() { return name; }
    public List<Mitarbeiter> getMitarbeiter() { return new ArrayList<>(mitarbeiter); }
}

public class Mitarbeiter {
    private String name;
    private String position;
    private Abteilung abteilung; // Сотрудник может существовать без отдела
    
    public Mitarbeiter(String name, String position) {
        this.name = name;
        this.position = position;
        this.abteilung = null; // Сотрудник может быть создан без отдела
    }
    
    public void setAbteilung(Abteilung abteilung) {
        this.abteilung = abteilung;
    }
    
    public void arbeiten() {
        String abteilungsName = abteilung != null ? abteilung.getName() : "keine Abteilung";
        System.out.println(name + " arbeitet als " + position + " in " + abteilungsName);
    }
    
    // Getter
    public String getName() { return name; }
    public Abteilung getAbteilung() { return abteilung; }
}

c) Композиция

Описание: Это ещё более строгая форма отношения “является частью”. Части не могут существовать без целого. Целое отвечает за время жизни частей. Строгое владение компонентами.

UML-диаграмма: Сплошная линия с закрашенным ромбом со стороны целого.

Пример: Автомобиль состоит из Двигателя. Двигатель не имеет самостоятельного существования без автомобиля. Когда автомобиль разбирают на металл, двигатель также уничтожается. Время жизни сильно связано.

Реализация: Целое создаёт части в собственном конструкторе.

// Пример композиции
public class Auto {
    private String marke;
    private String modell;
    private Motor motor; // Auto КОМПОНИРУЕТ Motor
    private List<Rad> raeder; // Auto КОМПОНИРУЕТ Räder
    
    public Auto(String marke, String modell) {
        this.marke = marke;
        this.modell = modell;
        
        // Части создаются вместе с Auto (строгая связь времени жизни)
        this.motor = new Motor(2.0, 150); // Motor создаётся с Auto
        this.raeder = new ArrayList<>();
        
        // Создаются 4 колеса
        for (int i = 0; i < 4; i++) {
            raeder.add(new Rad("225/45R17"));
        }
    }
    
    public void starten() {
        motor.starten();
        System.out.println(marke + " " + modell + " wird gestartet");
    }
    
    public void verschrotten() {
        // Все части уничтожаются, когда Auto разбирают
        motor.zerstoeren();
        for (Rad rad : raeder) {
            rad.zerstoeren();
        }
        raeder.clear();
        System.out.println("Auto und alle Teile wurden verschrottet");
    }
    
    // Getter
    public String getMarke() { return marke; }
    public Motor getMotor() { return motor; }
}

public class Motor {
    private double hubraum;
    private int leistungPS;
    private boolean laeuft;
    
    public Motor(double hubraum, int leistungPS) {
        this.hubraum = hubraum;
        this.leistungPS = leistungPS;
        this.laeuft = false;
    }
    
    public void starten() {
        this.laeuft = true;
        System.out.println("Motor (" + hubraum + "L, " + leistungPS + "PS) gestartet");
    }
    
    public void zerstoeren() {
        System.out.println("Motor zerstört");
    }
    
    // Getter
    public double getHubraum() { return hubraum; }
    public int getLeistungPS() { return leistungPS; }
}

public class Rad {
    private String groesse;
    private double druck;
    
    public Rad(String groesse) {
        this.groesse = groesse;
        this.druck = 2.5;
    }
    
    public void zerstoeren() {
        System.out.println("Rad (" + groesse + ") zerstört");
    }
    
    // Getter
    public String getGroesse() { return groesse; }
}

d) Обобщение и специализация (Наследование)

Описание: Это отношение “является” реализуется через наследование.

Обобщение: Извлечение общих свойств нескольких классов в более общий суперкласс (например, Собака, КошкаЖивотное).

Специализация: Создание более специфичного класса (подкласса) на основе более общего класса (суперкласса). Подкласс наследует все свойства и уточняет или расширяет их (например, ЖивотноеСобака; Собака добавляет метод лаять()).

UML-диаграмма: Сплошная линия с пустой стрелкой от подкласса к суперклассу.

Пример: Менеджер является Сотрудником. Он наследует все атрибуты (имя, зарплата) и может добавить атрибут бонус.

// Пример наследования
public class Mitarbeiter {
    protected String name;
    protected double grundgehalt;
    protected int mitarbeiterId;
    private static int naechsteId = 1;
    
    public Mitarbeiter(String name, double grundgehalt) {
        this.name = name;
        this.grundgehalt = grundgehalt;
        this.mitarbeiterId = naechsteId++;
    }
    
    public void arbeiten() {
        System.out.println(name + " arbeitet für " + grundgehalt + "€ Grundgehalt");
    }
    
    public double berechneGehalt() {
        return grundgehalt;
    }
    
    // Getter
    public String getName() { return name; }
    public int getMitarbeiterId() { return mitarbeiterId; }
}

public class Manager extends Mitarbeiter {
    private double bonus;
    private List<Mitarbeiter> team;
    
    public Manager(String name, double grundgehalt, double bonus) {
        super(name, grundgehalt); // Вызов конструктора суперкласса
        this.bonus = bonus;
        this.team = new ArrayList<>();
    }
    
    public void addTeamMitglied(Mitarbeiter mitarbeiter) {
        team.add(mitarbeiter);
    }
    
    @Override
    public void arbeiten() {
        System.out.println(name + " manages Team von " + team.size() + " Mitarbeitern");
    }
    
    @Override
    public double berechneGehalt() {
        return grundgehalt + bonus; // Дополнительный расчёт
    }
    
    public void teamMeeting() {
        System.out.println(name + " führt Team-Meeting durch");
        for (Mitarbeiter m : team) {
            System.out.println("- " + m.getName());
        }
    }
    
    // Getter
    public double getBonus() { return bonus; }
    public List<Mitarbeiter> getTeam() { return new ArrayList<>(team); }
}

public class Entwickler extends Mitarbeiter {
    private List<String> programmiersprachen;
    
    public Entwickler(String name, double grundgehalt, List<String> sprachen) {
        super(name, grundgehalt);
        this.programmiersprachen = new ArrayList<>(sprachen);
    }
    
    @Override
    public void arbeiten() {
        System.out.println(name + " programmiert in " + programmiersprachen);
    }
    
    public void lerneNeueSprache(String sprache) {
        programmiersprachen.add(sprache);
        System.out.println(name + " lernt " + sprache);
    }
    
    // Getter
    public List<String> getProgrammiersprachen() { return new ArrayList<>(programmiersprachen); }
}

3. Статические и нестатические методы/атрибуты

Это различие фундаментально и определяет, принадлежит ли свойство классу или объекту.

СвойствоНестатическое (член экземпляра)Статическое (член класса)
ПринадлежностьПринадлежит отдельному объекту (экземпляру)Принадлежит самому классу
Копии в памятиОдна копия на объект. 100 объектов = 100 копий атрибутаТолько одна копия для всего класса. Разделяется всеми объектами
ВызовЧерез объект: имяОбъекта.имяМетода()Через имя класса: ИмяКласса.имяМетода()
Доступ кМожет получить доступ к статическим и нестатическим членамМожет получить доступ только к статическим членам. Нет доступа к нестатическим (какой объект имелся в виду?)
Типовое использованиеАтрибуты, различающиеся от объекта к объекту (например, балансСчёта, имя)Служебные методы (например, Math.sqrt()), константы (например, Math.PI), счётчики (например, количествоСозданныхОбъектов)
// Пример статического vs нестатического
public class Student {
    // Нестатический атрибут (один на объект)
    private String name;
    private int matrikelNr;
    
    // Статический атрибут (один на класс)
    private static int anzahlStudenten = 0;
    private static final String UNIVERSITAET = "Technische Universität";
    
    public Student(String name, int matrikelNr) {
        this.name = name;
        this.matrikelNr = matrikelNr;
        anzahlStudenten++; // Доступ к статическому атрибуту
    }
    
    // Нестатический метод
    public void studieren() {
        System.out.println(name + " studiert an " + UNIVERSITAET);
        System.out.println("Aktuelle Anzahl Studenten: " + anzahlStudenten);
    }
    
    // Статический метод
    public static int getAnzahlStudenten() {
        return anzahlStudenten;
        // return name; // ОШИБКА: name нестатический и недоступен!
    }
    
    // Статический служебный метод
    public static boolean istGueltigeMatrikelNr(int nummer) {
        return nummer >= 100000 && nummer <= 999999;
    }
    
    // Getter и Setter
    public String getName() { return name; }
    public int getMatrikelNr() { return matrikelNr; }
    
    public static String getUniversitaet() { return UNIVERSITAET; }
}

// Использование
public class Main {
    public static void main(String[] args) {
        // Создание объектов
        Student stud1 = new Student("Max Mustermann", 123456);
        Student stud2 = new Student("Erika Mustermann", 234567);
        
        // Вызов нестатических методов через объекты
        stud1.studieren();
        stud2.studieren();
        
        // Вызов статических методов через класс
        System.out.println("Anzahl Studenten: " + Student.getAnzahlStudenten());
        System.out.println("Universität: " + Student.getUniversitaet());
        
        // Статический служебный метод
        boolean gueltig = Student.istGueltigeMatrikelNr(123456);
        System.out.println("Matrikelnummer gültig: " + gueltig);
        
        // Доступ к нестатическим атрибутам
        System.out.println("Student 1 Name: " + stud1.getName());
        
        // ОШИБКА: Статический метод не может быть вызван на объекте
        // Student.getName(); // Ошибка компиляции!
    }
}

4. Универсальные классы (Generics) — например List<T>

В чём проблема?

До появления Generics коллекции (вроде ArrayList) определялись для типа Object. Туда можно было добавить что угодно (строки, целые числа и т.д.). При извлечении пришлось вручную приводить тип (String s = (String) myList.get(0);). Ошибки на этапе выполнения происходили постоянно.

Решение?

Универсальные классы. Это шаблоны классов, которые используют один или несколько заполнителей для типов данных (часто T как “Type”, E как “Element”).

Цель: проверка типов на этапе компиляции. Компилятор следит, чтобы добавлялись только объекты правильного типа. Приведение типов не требуется, ошибки выполнения исключаются.

// Пример с Generics

// Без Generics (устарело, подвержено ошибкам)
List myOldList = new ArrayList();
myOldList.add("Hallo");
myOldList.add(123); // Компилятор ничего не говорит, но...
String s = (String) myOldList.get(1); // Ошибка выполнения: ClassCastException!

// С Generics (типобезопасно)
List<String> myList = new ArrayList<>(); // T становится String
myList.add("Hallo");
// myList.add(123); // ОШИБКА КОМПИЛЯЦИИ: 123 не String!
String s = myList.get(0); // Никакого приведения, всё безопасно.

// Собственный универсальный класс
public class Box<T> {
    private T inhalt;
    
    public void setInhalt(T inhalt) {
        this.inhalt = inhalt;
    }
    
    public T getInhalt() {
        return inhalt;
    }
    
    public boolean istLeer() {
        return inhalt == null;
    }
}

// Использование универсального класса
public class GenericsBeispiel {
    public static void main(String[] args) {
        // Box для строк
        Box<String> stringBox = new Box<>();
        stringBox.setInhalt("Hallo Welt");
        String inhalt = stringBox.getInhalt(); // Приведение не нужно
        
        // Box для Integer
        Box<Integer> integerBox = new Box<>();
        integerBox.setInhalt(42);
        Integer zahl = integerBox.getInhalt(); // Приведение не нужно
        
        // Box для собственных объектов
        Box<Student> studentBox = new Box<>();
        studentBox.setInhalt(new Student("Max", 123456));
        Student student = studentBox.getInhalt();
        
        System.out.println("String Box: " + inhalt);
        System.out.println("Integer Box: " + zahl);
        System.out.println("Student Box: " + student.getName());
    }
}

// Универсальные методы
public class Utility {
    // Универсальный метод обмена
    public static <T> void tausche(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    // Универсальный метод для максимума
    public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
        T max = x;
        if (y.compareTo(max) > 0) max = y;
        if (z.compareTo(max) > 0) max = z;
        return max;
    }
}

5. Преимущества универсальных контейнеров (Templates в C++) над массивами

ПризнакМассивыУниверсальные контейнеры (например ArrayList<T>, List<T>)
РазмерФиксированный. Размер определяется при создании и потом не меняетсяДинамический. Размер автоматически подстраивается под количество элементов
ТипобезопасностьОбеспечивают базовую проверку типов, но работают только с одним фиксированным типомОбеспечивают полную проверку типов на этапе компиляции через Generics
ФункциональностьОчень ограничена. Только базовые операции (чтение, запись по индексу)Множество полезных методов: .add(), .remove(), .contains(), .size() и т.д.
ПроизводительностьОчень быстро при прямом доступе по индексуНемного медленнее из-за overhead управления памятью, но в большинстве случаев это незначительно
ГибкостьНизкаяОчень высокая. Есть разные контейнеры для разных целей (списки, множества, отображения, очереди)
// Сравнение: массив vs. ArrayList
public class ArrayVsContainer {
    public static void main(String[] args) {
        // Массив - фиксированный размер
        String[] namenArray = new String[3];
        namenArray[0] = "Alice";
        namenArray[1] = "Bob";
        namenArray[2] = "Charlie";
        // namenArray[3] = "David"; // ОШИБКА: ArrayIndexOutOfBoundsException!
        
        // ArrayList - динамический размер
        ArrayList<String> namenList = new ArrayList<>();
        namenList.add("Alice");
        namenList.add("Bob");
        namenList.add("Charlie");
        namenList.add("David"); // Без проблем!
        namenList.add("Eve");   // Добавляет сколько угодно элементов!
        
        // Сравнение функциональности
        System.out.println("Array Länge: " + namenArray.length);
        System.out.println("ArrayList Größe: " + namenList.size());
        
        // ArrayList имеет больше методов
        namenList.remove("Bob"); // Удалить элемент
        boolean enthaeltAlice = namenList.contains("Alice"); // Проверить
        Collections.sort(namenList); // Отсортировать
        
        System.out.println("ArrayList nach Entfernung und Sortierung: " + namenList);
    }
}

Главное преимущество: универсальные контейнеры объединяют типобезопасность с гибкостью динамической структуры данных и поэтому в подавляющем большинстве случаев превосходят массивы.

Краткое резюме для экзамена IHK

  • Составляющие класса: имя, атрибуты, конструктор, методы
  • Отношения:
    • Ассоциация: отношение “знает”
    • Агрегация: “имеет” (слабая, часть существует независимо)
    • Композиция: “состоит из” (сильная, часть удаляется вместе)
    • Наследование: “является” (обобщение/специализация)
  • Статическое vs. нестатическое: класс vs. объект
  • Generics: делают классы типобезопасными, используя заполнители для типов данных
  • Контейнеры vs. массивы: контейнеры динамические, типобезопасные и функционально богаче

Концепции, относящиеся к экзамену

Важные различия

КонцепцияОписаниеUML-символВремя жизни
АссоциацияОтношение “знает” между независимыми классамиЛинияНезависимо
АгрегацияОтношение “имеет”, слабая связьЛиния с пустым ромбомНезависимо
КомпозицияОтношение “состоит из”, сильная связьЛиния с заполненным ромбомЗависимо
НаследованиеОтношение “является”, переиспользование кодаЛиния со стрелкойУнаследовано

Типичные задания на экзамене

  1. Нарисуйте UML-диаграммы классов с разными отношениями
  2. Реализуйте ассоциацию, агрегацию и композицию
  3. Объясните разницу между статическими и нестатическими членами
  4. Используйте Generics для типобезопасных контейнеров
  5. Сравните массивы с универсальными контейнерами

Эти концепции фундаментальны для понимания объектно-ориентированной архитектуры и служат основой для сложных системных проектов.

Продолжайте изучение OOP

Все статьи по OOP теперь завершены. Вернитесь к первой статье: Объектно-ориентированное программирование OOP основы.

Назад к блогу
Share:

Похожие статьи