Основы полиморфизма в UML: динамическая диспетчеризация и переопределение методов
Полиморфизм — ключевое понятие объектно-ориентированного программирования и моделирования в UML. Он позволяет объектам разных классов одинаково реагировать на одни и те же сообщения, но с разными результатами.
Что такое полиморфизм?
Полиморфизм (множественность форм) описывает способность объектов использовать одинаковый интерфейс, но с различными реализациями. В UML это представляется через иерархии наследования и интерфейсы.
Виды полиморфизма
- Переопределение (Override): метод подкласса заменяет метод базового класса
- Перегрузка (Overload): несколько методов с одинаковым названием, но разными параметрами
- Параметрический полиморфизм: обобщённые типы для типобезопасного переиспользования кода
- Специальный полиморфизм: перегрузка методов и преобразование типов
Представление полиморфизма в UML
Диаграмма классов с полиморфизмом
@startuml
abstract class Shape {
-color: String
-x: double
-y: double
+Shape(color: String, x: double, y: double)
+move(dx: double, dy: double): void
+area(): double {abstract}
+perimeter(): double {abstract}
+toString(): String
}
class Rectangle {
-width: double
-height: double
+Rectangle(color: String, x: double, y: double, width: double, height: double)
+area(): double
+perimeter(): double
+setDimensions(width: double, height: double): void
+toString(): String
}
class Circle {
-radius: double
+Circle(color: String, x: double, y: double, radius: double)
+area(): double
+perimeter(): double
+setRadius(radius: double): void
+toString(): String
}
class Triangle {
-base: double
-height: double
+Triangle(color: String, x: double, y: double, base: double, height: double)
+area(): double
+perimeter(): double
+toString(): String
}
Shape <|-- Rectangle
Shape <|-- Circle
Shape <|-- Triangle
@enduml
Диаграмма последовательности для динамической диспетчеризации
@startuml
actor User
User -> ShapeProcessor: processShapes(shapes)
activate ShapeProcessor
loop для каждой фигуры
ShapeProcessor -> Shape: area()
activate Shape
alt Rectangle
Shape --> ShapeProcessor: Rectangle.area()
else Circle
Shape --> ShapeProcessor: Circle.area()
else Triangle
Shape --> ShapeProcessor: Triangle.area()
end
deactivate Shape
ShapeProcessor -> Shape: perimeter()
activate Shape
alt Rectangle
Shape --> ShapeProcessor: Rectangle.perimeter()
else Circle
Shape --> ShapeProcessor: Circle.perimeter()
else Triangle
Shape --> ShapeProcessor: Triangle.perimeter()
end
deactivate Shape
end
ShapeProcessor --> User: Результаты
deactivate ShapeProcessor
@enduml
Динамическая диспетчеризация в Java
Переопределение методов и динамический вызов
public class PolymorphismDemo {
// Абстрактный базовый класс
public abstract class Shape {
protected String color;
protected double x, y;
public Shape(String color, double x, double y) {
this.color = color;
this.x = x;
this.y = y;
}
// Метод, который можно переопределить
public void move(double dx, double dy) {
this.x += dx;
this.y += dy;
System.out.println(color + " фигура сдвинута в (" + x + ", " + y + ")");
}
// Абстрактные методы, которые обязательно переопределить
public abstract double area();
public abstract double perimeter();
// Конкретный метод, который можно переопределить
public String getDescription() {
return "Фигура " + color + " в позиции (" + x + ", " + y + ")";
}
// Геттеры
public String getColor() { return color; }
public double getX() { return x; }
public double getY() { return y; }
}
// Прямоугольник, переопределяет абстрактные методы
public class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double x, double y, double width, double height) {
super(color, x, y);
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
@Override
public String getDescription() {
return super.getDescription() + " (прямоугольник " + width + "x" + height + ")";
}
// Дополнительный метод
public void setDimensions(double width, double height) {
this.width = width;
this.height = height;
}
}
// Круг, переопределяет абстрактные методы
public class Circle extends Shape {
private double radius;
public Circle(String color, double x, double y, double radius) {
super(color, x, y);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
@Override
public String getDescription() {
return super.getDescription() + " (круг с радиусом " + radius + ")";
}
public void setRadius(double radius) {
this.radius = radius;
}
}
// Демонстрация динамической диспетчеризации
public void demonstrateDynamicBinding() {
List<Shape> shapes = new ArrayList<>();
shapes.add(new Rectangle("красный", 0, 0, 5, 3));
shapes.add(new Circle("синий", 10, 10, 2));
shapes.add(new Rectangle("зелёный", 5, 5, 2, 2));
// Полиморфная обработка — динамический вызов метода
for (Shape shape : shapes) {
System.out.println(shape.getDescription());
// Динамическая диспетчеризация — в зависимости от типа объекта вызывается нужный метод
double area = shape.area(); // Вызывает Rectangle.area() или Circle.area()
double perimeter = shape.perimeter(); // Вызывает Rectangle.perimeter() или Circle.perimeter()
System.out.println(" Площадь: " + String.format("%.2f", area));
System.out.println(" Периметр: " + String.format("%.2f", perimeter));
// move() тоже может быть переопределён
shape.move(1, 1);
System.out.println();
}
}
}
Перегрузка методов
Перегрузка в Java
public class MethodOverloading {
// Перегруженные методы для разных типов параметров
public class Calculator {
// Перегрузка для int
public int add(int a, int b) {
System.out.println("int add(int, int) вызван");
return a + b;
}
// Перегрузка для double
public double add(double a, double b) {
System.out.println("double add(double, double) вызван");
return a + b;
}
// Перегрузка для трёх параметров
public int add(int a, int b, int c) {
System.out.println("int add(int, int, int) вызван");
return a + b + c;
}
// Перегрузка для массива
public int add(int[] numbers) {
System.out.println("int add(int[]) вызван");
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}
// Перегрузка с переменным числом аргументов
public int addVarargs(int... numbers) {
System.out.println("int addVarargs(int...) вызван");
return add(numbers);
}
// Перегрузка для разных типов объектов
public String concatenate(String a, String b) {
System.out.println("String concatenate(String, String) вызван");
return a + b;
}
public String concatenate(String a, String b, String c) {
System.out.println("String concatenate(String, String, String) вызван");
return a + b + c;
}
}
// Демонстрация перегрузки
public void demonstrateOverloading() {
Calculator calc = new Calculator();
// Разные перегруженные версии вызываются автоматически
System.out.println("5 + 3 = " + calc.add(5, 3));
System.out.println("5.5 + 3.3 = " + calc.add(5.5, 3.3));
System.out.println("1 + 2 + 3 = " + calc.add(1, 2, 3));
System.out.println("Сумма массива = " + calc.add(new int[]{1, 2, 3, 4, 5}));
System.out.println("Сумма varargs = " + calc.addVarargs(1, 2, 3, 4, 5));
System.out.println("Hello + World = " + calc.concatenate("Hello", "World"));
System.out.println("A + B + C = " + calc.concatenate("A", "B", "C"));
}
}
Перегрузка с наследованием
public class OverloadingWithInheritance {
public class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
public void makeSound(String intensity) {
System.out.println("Animal makes a " + intensity + " sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
// Перегрузка, не переопределение
public void makeSound(String intensity) {
System.out.println("Dog barks " + intensity);
}
// Дополнительная перегрузка
public void makeSound(String intensity, int times) {
for (int i = 0; i < times; i++) {
System.out.println("Dog barks " + intensity);
}
}
}
public void demonstrateOverloadingInheritance() {
Animal animal = new Animal();
Dog dog = new Dog();
Animal animalDog = new Dog(); // Upcasting
// Статическая привязка при перегрузке (во время компиляции)
animal.makeSound(); // Animal makes a sound
animal.makeSound("loud"); // Animal makes a loud sound
dog.makeSound(); // Dog barks (переопределено)
dog.makeSound("loud"); // Dog barks loud (перегружено)
dog.makeSound("loud", 3); // Dog barks loud (3x) (перегружено)
// Важно: статическая привязка при перегрузке!
animalDog.makeSound(); // Dog barks (динамическая привязка)
animalDog.makeSound("loud"); // Animal makes a loud sound (статическая привязка!)
}
}
Generics и параметрический полиморфизм
Generic классы
public class GenericPolymorphism {
// Generic контейнер
public class Container<T> {
private T content;
private String label;
public Container(String label, T content) {
this.label = label;
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
public String getLabel() {
return label;
}
// Generic метод
public <U> Container<U> transform(Function<T, U> transformer) {
U newContent = transformer.apply(content);
return new Container<>(label, newContent);
}
@Override
public String toString() {
return label + ": " + content;
}
}
// Generic процессор
public class Processor<T> {
public List<T> filter(List<T> items, Predicate<T> predicate) {
return items.stream()
.filter(predicate)
.collect(Collectors.toList());
}
public <R> List<R> map(List<T> items, Function<T, R> mapper) {
return items.stream()
.map(mapper)
.collect(Collectors.toList());
}
public T reduce(List<T> items, BinaryOperator<T> accumulator, T identity) {
return items.stream()
.reduce(identity, accumulator);
}
}
// Демонстрация
public void demonstrateGenerics() {
// Контейнер с разными типами
Container<String> stringContainer = new Container<>("Text", "Hello World");
Container<Integer> intContainer = new Container<>("Zahl", 42);
Container<List<String>> listContainer = new Container<>("Liste",
Arrays.asList("A", "B", "C"));
System.out.println(stringContainer);
System.out.println(intContainer);
System.out.println(listContainer);
// Трансформация с generic методом
Container<Integer> lengthContainer = stringContainer.transform(String::length);
System.out.println("Länge: " + lengthContainer);
// Generic процессор
Processor<String> stringProcessor = new Processor<>();
List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
// Фильтрация
List<String> longWords = stringProcessor.filter(words, s -> s.length() > 5);
System.out.println("Lange Wörter: " + longWords);
// Преобразование
List<Integer> lengths = stringProcessor.map(words, String::length);
System.out.println("Längen: " + lengths);
// Редукция
String concatenated = stringProcessor.reduce(words, String::concat, "");
System.out.println("Zusammengefügt: " + concatenated);
}
}
Generic методы
public class GenericMethods {
// Generic метод для сравнения
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// Generic метод для обмена
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Generic метод для конвертации
public static <T, R> List<R> convertList(List<T> list, Function<T, R> converter) {
return list.stream()
.map(converter)
.collect(Collectors.toList());
}
// Generic метод с wildcards
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// Wildcard с верхней границей
public static double sumOfNumbers(List<? extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// Wildcard с нижней границей
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// Демонстрация
public static void demonstrateGenericMethods() {
// Max метод
System.out.println("Max von 5 und 3: " + max(5, 3));
System.out.println("Max von 'Hello' und 'World': " + max("Hello", "World"));
// Swap метод
String[] words = {"A", "B", "C"};
System.out.println("Vor Swap: " + Arrays.toString(words));
swap(words, 0, 2);
System.out.println("Nach Swap: " + Arrays.toString(words));
// Convert метод
List<String> strings = Arrays.asList("1", "2", "3", "4", "5");
List<Integer> integers = convertList(strings, Integer::parseInt);
System.out.println("Konvertiert: " + integers);
// Wildcard методы
List<String> stringList = Arrays.asList("A", "B", "C");
List<Integer> intList = Arrays.asList(1, 2, 3);
System.out.println("String Liste:");
printList(stringList);
System.out.println("Integer Liste:");
printList(intList);
// Wildcard с верхней границей
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("Summe: " + sumOfNumbers(doubles));
// Wildcard с нижней границей
List<Number> numbers = new ArrayList<>();
addNumbers(numbers);
System.out.println("Zahlen hinzugefügt: " + numbers);
}
}
Интерфейсы и полиморфизм
Полиморфизм на основе интерфейсов
public class InterfacePolymorphism {
// Interface для полиморфного поведения
public interface Drawable {
void draw();
double getArea();
String getType();
}
public interface Movable {
void move(double dx, double dy);
void setPosition(double x, double y);
double[] getPosition();
}
// Класс реализует несколько интерфейсов
public class Circle implements Drawable, Movable {
private double radius, x, y;
public Circle(double radius, double x, double y) {
this.radius = radius;
this.x = x;
this.y = y;
}
@Override
public void draw() {
System.out.println("Zeichne Kreis an (" + x + ", " + y + ") mit Radius " + radius);
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public String getType() {
return "Kreis";
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
System.out.println("Kreis verschoben nach (" + x + ", " + y + ")");
}
@Override
public void setPosition(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public double[] getPosition() {
return new double[]{x, y};
}
}
public class Rectangle implements Drawable, Movable {
private double width, height, x, y;
public Rectangle(double width, double height, double x, double y) {
this.width = width;
this.height = height;
this.x = x;
this.y = y;
}
@Override
public void draw() {
System.out.println("Zeichne Rechteck an (" + x + ", " + y + ") " + width + "x" + height);
}
@Override
public double getArea() {
return width * height;
}
@Override
public String getType() {
return "Rechteck";
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
System.out.println("Rechteck verschoben nach (" + x + ", " + y + ")");
}
@Override
public void setPosition(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public double[] getPosition() {
return new double[]{x, y};
}
}
// Полиморфная обработка
public void processShapes(List<Drawable> shapes) {
for (Drawable shape : shapes) {
shape.draw();
System.out.println("Тип: " + shape.getType());
System.out.println("Площадь: " + shape.getArea());
System.out.println();
}
}
public void moveShapes(List<Movable> movables, double dx, double dy) {
for (Movable movable : movables) {
movable.move(dx, dy);
}
}
// Демонстрация
public void demonstrateInterfacePolymorphism() {
List<Drawable> shapes = new ArrayList<>();
List<Movable> movables = new ArrayList<>();
Circle circle = new Circle(2.0, 0, 0);
Rectangle rectangle = new Rectangle(3.0, 4.0, 5, 5);
shapes.add(circle);
shapes.add(rectangle);
movables.add(circle);
movables.add(rectangle);
System.out.println("=== Formen zeichnen ===");
processShapes(shapes);
System.out.println("=== Formen verschieben ===");
moveShapes(movables, 10, 10);
System.out.println("=== Nach dem Verschieben ===");
processShapes(shapes);
}
}
UML-нотация для полиморфизма
Соглашения диаграмм классов
@startuml
' Полиморфное отношение в UML
abstract class PaymentProcessor {
+processPayment(amount: double): boolean {abstract}
+validatePayment(amount: double): boolean {abstract}
}
class CreditCardProcessor {
+processPayment(amount: double): boolean
+validatePayment(amount: double): boolean
+validateCardNumber(number: String): boolean
}
class PayPalProcessor {
+processPayment(amount: double): boolean
+validatePayment(amount: double): boolean
+validateEmail(email: String): boolean
}
class BankTransferProcessor {
+processPayment(amount: double): boolean
+validatePayment(amount: double): boolean
+validateBankDetails(iban: String): boolean
}
PaymentProcessor <|-- CreditCardProcessor
PaymentProcessor <|-- PayPalProcessor
PaymentProcessor <|-- BankTransferProcessor
' Interface для полиморфных операций
interface Refundable {
+processRefund(amount: double): boolean
+getRefundStatus(): String
}
CreditCardProcessor ..|> Refundable
PayPalProcessor ..|> Refundable
BankTransferProcessor ..|> Refundable
@enduml
Диаграмма последовательности для полиморфных вызовов
@startuml
actor Customer
Customer -> PaymentSystem: makePayment(amount, method)
activate PaymentSystem
PaymentSystem -> PaymentProcessorFactory: createProcessor(method)
activate PaymentProcessorFactory
PaymentProcessorFactory --> PaymentSystem: processor
deactivate PaymentProcessorFactory
PaymentSystem -> PaymentProcessor: processPayment(amount)
activate PaymentProcessor
alt Credit Card
PaymentProcessor -> CreditCardProcessor: processPayment(amount)
CreditCardProcessor --> PaymentProcessor: success
else PayPal
PaymentProcessor -> PayPalProcessor: processPayment(amount)
PayPalProcessor --> PaymentProcessor: success
else Bank Transfer
PaymentProcessor -> BankTransferProcessor: processPayment(amount)
BankTransferProcessor --> PaymentProcessor: success
end
PaymentProcessor --> PaymentSystem: result
deactivate PaymentProcessor
PaymentSystem --> Customer: payment result
deactivate PaymentSystem
@enduml
Лучшие практики полиморфизма
1. Принцип подстановки Лисков
// Хорошо: Rectangle может заменить Shape везде
public class GoodPolymorphism {
public interface Shape {
double area();
double perimeter();
void move(double dx, double dy);
}
public class Rectangle implements Shape {
private double width, height, x, y;
public Rectangle(double width, double height, double x, double y) {
this.width = width;
this.height = height;
this.x = x;
this.y = y;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
}
// Дополнительные методы не нарушают LSP
public double getWidth() { return width; }
public double getHeight() { return height; }
}
public class Square implements Shape {
private double side, x, y;
public Square(double side, double x, double y) {
this.side = side;
this.x = x;
this.y = y;
}
@Override
public double area() {
return side * side;
}
@Override
public double perimeter() {
return 4 * side;
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
}
public double getSide() { return side; }
}
}
2. Разделение интерфейса
// Плохо: Слишком большой интерфейс
public interface BadShape {
double area();
double perimeter();
void move(double dx, double dy);
void rotate(double angle);
void resize(double factor);
Color getColor();
void setColor(Color color);
}
// Хорошо: Специализированные интерфейсы
public interface Drawable {
void draw(Graphics g);
}
public interface Movable {
void move(double dx, double dy);
void setPosition(double x, double y);
double[] getPosition();
}
public interface Resizable {
void resize(double factor);
void setSize(double width, double height);
}
public interface Rotatable {
void rotate(double angle);
double getRotation();
}
public interface Colored {
Color getColor();
void setColor(Color color);
}
// Класс реализует только нужные интерфейсы
public class Circle implements Drawable, Movable, Resizable, Colored {
// Реализация...
}
3. Шаблонный метод
public abstract class DataProcessor {
// Шаблонный метод - определяет алгоритм
public final void processData() {
loadData();
if (validateData()) {
transformData();
saveData();
onSuccess();
} else {
onError();
}
cleanup();
}
// Абстрактные методы - должны быть реализованы
protected abstract void loadData();
protected abstract boolean validateData();
protected abstract void transformData();
protected abstract void saveData();
// Методы-перехватчики - могут быть переопределены
protected void onSuccess() {
System.out.println("Обработка успешна");
}
protected void onError() {
System.out.println("Обработка не удалась");
}
protected void cleanup() {
System.out.println("Очистка ресурсов");
}
}
public class CSVProcessor extends DataProcessor {
@Override
protected void loadData() {
System.out.println("Загружаю CSV данные");
}
@Override
protected boolean validateData() {
System.out.println("Проверяю CSV данные");
return true;
}
@Override
protected void transformData() {
System.out.println("Трансформирую CSV данные");
}
@Override
protected void saveData() {
System.out.println("Сохраняю CSV данные");
}
@Override
protected void onSuccess() {
System.out.println("CSV успешно обработан!");
}
}
Концепции для экзамена
Важные различия
-
Переопределение vs Перегрузка
- Переопределение: одинаковая сигнатура в подклассе
- Перегрузка: одинаковое имя, разные параметры
-
Статическое vs Динамическое связывание
- Статическое: перегрузка (на этапе компиляции)
- Динамическое: переопределение (во время выполнения)
-
Абстрактный класс vs Интерфейс
- Абстрактный класс: общая реализация
- Интерфейс: чистый контракт
-
Generics vs Наследование
- Generics: типобезопасность на этапе компиляции
- Наследование: полиморфизм во время выполнения
Типичные задачи на экзамене
- Рисуйте UML диаграммы для полиморфных отношений
- Реализуйте переопределённые методы
- Объясните динамическое связывание
- Сравните различные типы полиморфизма
- Проектируйте полиморфные иерархии классов
Итог
Полиморфизм - мощный инструмент для создания гибкой архитектуры:
- Переопределение обеспечивает динамическое связывание и полиморфизм во время выполнения
- Перегрузка даёт статическое связывание и полиморфизм на этапе компиляции
- Generics позволяют безопасно переиспользовать код
- Интерфейсы определяют полиморфные контракты
Хороший полиморфизм требует соблюдения принципа Лисков и продуманного проектирования интерфейсов, что гарантирует поддерживаемость и расширяемость кода.
Продолжение в пути обучения UML
Следующая статья в пути обучения UML посвящена UML диаграммам классов: отношения - Association, Aggregation, Composition, Dependency, Inheritance и детально рассматривает различные типы отношений в диаграммах классов.

