UML Polymorphie Grundlagen: Dynamische Bindung & Überschreiben
Polymorphie ist ein zentrales Konzept der objektorientierten Programmierung und UML-Modellierung. Sie ermöglicht es Objekten verschiedener Klassen, auf dieselbe Nachricht unterschiedlich zu reagieren.
Was ist Polymorphie?
Polymorphie (Vielgestaltigkeit) beschreibt die Fähigkeit von Objekten, dieselbe Schnittstelle zu verwenden, aber unterschiedliche Implementierungen zu haben. In UML wird dies durch Vererbungshierarchien und Schnittstellen dargestellt.
Arten der Polymorphie
- Überschreiben (Override): Methode in Unterklasse ersetzt Basisklassenmethode
- Überladen (Overload): Mehrere Methoden gleichen Namens mit unterschiedlichen Parametern
- Parametrische Polymorphie: Generics für typsichere Wiederverwendung
- Ad-hoc Polymorphie: Methodenüberladung und Typ-Konvertierung
UML-Darstellung von Polymorphie
Klassendiagramm mit Polymorphie
@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
Sequenzdiagramm für dynamische Bindung
@startuml
actor User
User -> ShapeProcessor: processShapes(shapes)
activate ShapeProcessor
loop für jede Form
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: Ergebnisse
deactivate ShapeProcessor
@enduml
Dynamische Bindung in Java
Überschreiben und dynamischer Dispatch
public class PolymorphismDemo {
// Abstrakte Basisklasse
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;
}
// Überschreibbare Methode
public void move(double dx, double dy) {
this.x += dx;
this.y += dy;
System.out.println(color + " Form verschoben nach (" + x + ", " + y + ")");
}
// Abstrakte Methoden - müssen überschrieben werden
public abstract double area();
public abstract double perimeter();
// Konkrete Methode kann überschrieben werden
public String getDescription() {
return "Eine " + color + " Form an Position (" + x + ", " + y + ")";
}
// Getter
public String getColor() { return color; }
public double getX() { return x; }
public double getY() { return y; }
}
// Rectangle - überschreibt abstrakte Methoden
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() + " (Rechteck " + width + "x" + height + ")";
}
// Zusätzliche Methode
public void setDimensions(double width, double height) {
this.width = width;
this.height = height;
}
}
// Circle - überschreibt abstrakte Methoden
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() + " (Kreis mit Radius " + radius + ")";
}
public void setRadius(double radius) {
this.radius = radius;
}
}
// Dynamische Bindung Demonstration
public void demonstrateDynamicBinding() {
List<Shape> shapes = new ArrayList<>();
shapes.add(new Rectangle("rot", 0, 0, 5, 3));
shapes.add(new Circle("blau", 10, 10, 2));
shapes.add(new Rectangle("grün", 5, 5, 2, 2));
// Polymorphe Verarbeitung - dynamischer Dispatch
for (Shape shape : shapes) {
System.out.println(shape.getDescription());
// Dynamische Bindung - je nach Objekttyp wird die richtige Methode aufgerufen
double area = shape.area(); // Wählt Rectangle.area() oder Circle.area()
double perimeter = shape.perimeter(); // Wählt Rectangle.perimeter() oder Circle.perimeter()
System.out.println(" Fläche: " + String.format("%.2f", area));
System.out.println(" Umfang: " + String.format("%.2f", perimeter));
// Auch move() kann überschrieben werden
shape.move(1, 1);
System.out.println();
}
}
}
Methodenüberladung
Überladen in Java
public class MethodOverloading {
// Überladene Methoden für unterschiedliche Parametertypen
public class Calculator {
// Überladen für int
public int add(int a, int b) {
System.out.println("int add(int, int) aufgerufen");
return a + b;
}
// Überladen für double
public double add(double a, double b) {
System.out.println("double add(double, double) aufgerufen");
return a + b;
}
// Überladen für drei Parameter
public int add(int a, int b, int c) {
System.out.println("int add(int, int, int) aufgerufen");
return a + b + c;
}
// Überladen für Arrays
public int add(int[] numbers) {
System.out.println("int add(int[]) aufgerufen");
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}
// Überladen mit varargs
public int addVarargs(int... numbers) {
System.out.println("int addVarargs(int...) aufgerufen");
return add(numbers);
}
// Überladen für unterschiedliche Objekttypen
public String concatenate(String a, String b) {
System.out.println("String concatenate(String, String) aufgerufen");
return a + b;
}
public String concatenate(String a, String b, String c) {
System.out.println("String concatenate(String, String, String) aufgerufen");
return a + b + c;
}
}
// Überladung Demonstration
public void demonstrateOverloading() {
Calculator calc = new Calculator();
// Verschiedene Überladungen werden aufgerufen
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("Array sum = " + calc.add(new int[]{1, 2, 3, 4, 5}));
System.out.println("Varargs sum = " + 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"));
}
}
Überladung mit Vererbung
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");
}
// Überladen, nicht überschrieben
public void makeSound(String intensity) {
System.out.println("Dog barks " + intensity);
}
// Zusätzliche Überladung
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
// Statische Bindung bei Überladung (Compile-Time)
animal.makeSound(); // Animal makes a sound
animal.makeSound("loud"); // Animal makes a loud sound
dog.makeSound(); // Dog barks (überschrieben)
dog.makeSound("loud"); // Dog barks loud (überladen)
dog.makeSound("loud", 3); // Dog barks loud (3x) (überladen)
// Wichtig: Statische Bindung bei Überladung!
animalDog.makeSound(); // Dog barks (dynamische Bindung)
animalDog.makeSound("loud"); // Animal makes a loud sound (statische Bindung!)
}
}
Generics und parametrische Polymorphie
Generic Klassen
public class GenericPolymorphism {
// Generic Container Klasse
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 Methode
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 Processor
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);
}
}
// Demonstration
public void demonstrateGenerics() {
// Container mit verschiedenen Typen
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);
// Transformation mit Generic Methode
Container<Integer> lengthContainer = stringContainer.transform(String::length);
System.out.println("Länge: " + lengthContainer);
// Generic Processor
Processor<String> stringProcessor = new Processor<>();
List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
// Filtern
List<String> longWords = stringProcessor.filter(words, s -> s.length() > 5);
System.out.println("Lange Wörter: " + longWords);
// Mappen
List<Integer> lengths = stringProcessor.map(words, String::length);
System.out.println("Längen: " + lengths);
// Reduzieren
String concatenated = stringProcessor.reduce(words, String::concat, "");
System.out.println("Zusammengefügt: " + concatenated);
}
}
Generic Methoden
public class GenericMethods {
// Generic Methode für Vergleich
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// Generic Methode für Swap
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Generic Methode für Konvertierung
public static <T, R> List<R> convertList(List<T> list, Function<T, R> converter) {
return list.stream()
.map(converter)
.collect(Collectors.toList());
}
// Generic Methode mit wildcards
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// Upper Bounded Wildcard
public static double sumOfNumbers(List<? extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// Lower Bounded Wildcard
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// Demonstration
public static void demonstrateGenericMethods() {
// Max Methode
System.out.println("Max von 5 und 3: " + max(5, 3));
System.out.println("Max von 'Hello' und 'World': " + max("Hello", "World"));
// Swap Methode
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 Methode
List<String> strings = Arrays.asList("1", "2", "3", "4", "5");
List<Integer> integers = convertList(strings, Integer::parseInt);
System.out.println("Konvertiert: " + integers);
// Wildcard Methoden
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);
// Upper bounded wildcard
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("Summe: " + sumOfNumbers(doubles));
// Lower bounded wildcard
List<Number> numbers = new ArrayList<>();
addNumbers(numbers);
System.out.println("Zahlen hinzugefügt: " + numbers);
}
}
Interfaces und Polymorphie
Interface-basierte Polymorphie
public class InterfacePolymorphism {
// Interface für polymorphes Verhalten
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();
}
// Klasse implementiert mehrere Interfaces
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};
}
}
// Polymorphe Verarbeitung
public void processShapes(List<Drawable> shapes) {
for (Drawable shape : shapes) {
shape.draw();
System.out.println("Typ: " + shape.getType());
System.out.println("Fläche: " + shape.getArea());
System.out.println();
}
}
public void moveShapes(List<Movable> movables, double dx, double dy) {
for (Movable movable : movables) {
movable.move(dx, dy);
}
}
// Demonstration
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-Notation für Polymorphie
Klassendiagramm-Konventionen
@startuml
' Polymorphe Beziehung in 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 für polymorphe Operationen
interface Refundable {
+processRefund(amount: double): boolean
+getRefundStatus(): String
}
CreditCardProcessor ..|> Refundable
PayPalProcessor ..|> Refundable
BankTransferProcessor ..|> Refundable
@enduml
Sequenzdiagramm für polymorphe Aufrufe
@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
Best Practices für Polymorphie
1. Liskov Substitution Principle
// Gut: Rectangle kann Shape überall ersetzen
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;
}
// Zusätzliche Methoden verletzen LSP nicht
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. Interface Segregation
// Schlecht: Zu großes Interface
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);
}
// Gut: Spezialisierte Interfaces
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);
}
// Klasse implementiert nur benötigte Interfaces
public class Circle implements Drawable, Movable, Resizable, Colored {
// Implementierung...
}
3. Template Method Pattern
public abstract class DataProcessor {
// Template Method - definiert Algorithmus
public final void processData() {
loadData();
if (validateData()) {
transformData();
saveData();
onSuccess();
} else {
onError();
}
cleanup();
}
// Abstrakte Methoden - müssen implementiert werden
protected abstract void loadData();
protected abstract boolean validateData();
protected abstract void transformData();
protected abstract void saveData();
// Hook Methoden - können überschrieben werden
protected void onSuccess() {
System.out.println("Verarbeitung erfolgreich");
}
protected void onError() {
System.out.println("Verarbeitung fehlgeschlagen");
}
protected void cleanup() {
System.out.println("Aufräumen");
}
}
public class CSVProcessor extends DataProcessor {
@Override
protected void loadData() {
System.out.println("Lade CSV Daten");
}
@Override
protected boolean validateData() {
System.out.println("Validiere CSV Daten");
return true;
}
@Override
protected void transformData() {
System.out.println("Transformiere CSV Daten");
}
@Override
protected void saveData() {
System.out.println("Speichere CSV Daten");
}
@Override
protected void onSuccess() {
System.out.println("CSV Verarbeitung erfolgreich!");
}
}
Prüfungsrelevante Konzepte
Wichtige Unterscheidungen
-
Überschreiben vs Überladen
- Überschreiben: Gleiche Signatur in Unterklasse
- Überladen: Gleicher Name, unterschiedliche Parameter
-
Statische vs Dynamische Bindung
- Statisch: Überladung (Compile-Time)
- Dynamisch: Überschreiben (Runtime)
-
Abstrakte Klasse vs Interface
- Abstrakte Klasse: Gemeinsame Implementierung
- Interface: Reiner Vertrag
-
Generics vs Vererbung
- Generics: Typsicherheit bei Compile-Time
- Vererbung: Polymorphie bei Runtime
Typische Prüfungsaufgaben
- Zeichnen Sie UML-Diagramme für polymorphe Beziehungen
- Implementieren Sie überschriebene Methoden
- Erklären Sie dynamische Bindung
- Vergleichen Sie verschiedene Polymorphie-Arten
- Entwerfen Sie polymorphe Klassenhierarchien
Zusammenfassung
Polymorphie ist ein mächtiges Konzept für flexible Softwarearchitektur:
- Überschreiben ermöglicht dynamische Bindung und Runtime-Polymorphie
- Überladen bietet statische Bindung und Compile-Time-Polymorphie
- Generics ermöglichen typsichere Wiederverwendung
- Interfaces definieren polymorphe Verträge
Gute Polymorphie erfordert Einhaltung des Liskov Substitution Principles und sorgfältiges Interface Design für wartbare und erweiterbare Software.