Отношения между классами в ООП: ассоциация, агрегация, композиция и наследование
Отдельные объекты полезны, но настоящая мощь объектно-ориентированного программирования проявляется, когда объекты взаимодействуют друг с другом и работают вместе для решения сложных задач. В этом документе мы разберём структуру классов и различные типы отношений между ними — всё необходимое для подготовки к экзамену.
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-символ | Время жизни |
|---|---|---|---|
| Ассоциация | Отношение “знает” между независимыми классами | Линия | Независимо |
| Агрегация | Отношение “имеет”, слабая связь | Линия с пустым ромбом | Независимо |
| Композиция | Отношение “состоит из”, сильная связь | Линия с заполненным ромбом | Зависимо |
| Наследование | Отношение “является”, переиспользование кода | Линия со стрелкой | Унаследовано |
Типичные задания на экзамене
- Нарисуйте UML-диаграммы классов с разными отношениями
- Реализуйте ассоциацию, агрегацию и композицию
- Объясните разницу между статическими и нестатическими членами
- Используйте Generics для типобезопасных контейнеров
- Сравните массивы с универсальными контейнерами
Эти концепции фундаментальны для понимания объектно-ориентированной архитектуры и служат основой для сложных системных проектов.
Продолжайте изучение OOP
Все статьи по OOP теперь завершены. Вернитесь к первой статье: Объектно-ориентированное программирование OOP основы.

