UML Polymorphism Basics: Dynamic Binding & Overriding
Polymorphism is a central concept in object-oriented programming and UML modeling. It enables objects of different classes to respond differently to the same message.
What is Polymorphism?
Polymorphism (many-formedness) describes the ability of objects to use the same interface but have different implementations. In UML, this is represented through inheritance hierarchies and interfaces.
Types of Polymorphism
- Overriding (Override): Method in subclass replaces base class method
- Overloading (Overload): Multiple methods with the same name but different parameters
- Parametric Polymorphism: Generics for type-safe reusability
- Ad-hoc Polymorphism: Method overloading and type conversion
UML Representation of Polymorphism
Class Diagram with Polymorphism
@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
Sequence Diagram for Dynamic Binding
@startuml
actor User
User -> ShapeProcessor: processShapes(shapes)
activate ShapeProcessor
loop for each shape
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: Results
deactivate ShapeProcessor
@enduml
Dynamic Binding in Java
Overriding and Dynamic Dispatch
public class PolymorphismDemo {
// Abstract base class
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;
}
// Overridable method
public void move(double dx, double dy) {
this.x += dx;
this.y += dy;
System.out.println(color + " shape moved to (" + x + ", " + y + ")");
}
// Abstract methods - must be overridden
public abstract double area();
public abstract double perimeter();
// Concrete method can be overridden
public String getDescription() {
return "A " + color + " shape at position (" + x + ", " + y + ")";
}
// Getters
public String getColor() { return color; }
public double getX() { return x; }
public double getY() { return y; }
}
// Rectangle - overrides abstract methods
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() + " (Rectangle " + width + "x" + height + ")";
}
// Additional method
public void setDimensions(double width, double height) {
this.width = width;
this.height = height;
}
}
// Circle - overrides abstract methods
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() + " (Circle with radius " + radius + ")";
}
public void setRadius(double radius) {
this.radius = radius;
}
}
// Dynamic Binding Demonstration
public void demonstrateDynamicBinding() {
List<Shape> shapes = new ArrayList<>();
shapes.add(new Rectangle("red", 0, 0, 5, 3));
shapes.add(new Circle("blue", 10, 10, 2));
shapes.add(new Rectangle("green", 5, 5, 2, 2));
// Polymorphic processing - dynamic dispatch
for (Shape shape : shapes) {
System.out.println(shape.getDescription());
// Dynamic binding - the correct method is called based on object type
double area = shape.area(); // Selects Rectangle.area() or Circle.area()
double perimeter = shape.perimeter(); // Selects Rectangle.perimeter() or Circle.perimeter()
System.out.println(" Area: " + String.format("%.2f", area));
System.out.println(" Perimeter: " + String.format("%.2f", perimeter));
// move() can also be overridden
shape.move(1, 1);
System.out.println();
}
}
}
Method Overloading
Overloading in Java
public class MethodOverloading {
// Overloaded methods for different parameter types
public class Calculator {
// Overload for int
public int add(int a, int b) {
System.out.println("int add(int, int) called");
return a + b;
}
// Overload for double
public double add(double a, double b) {
System.out.println("double add(double, double) called");
return a + b;
}
// Overload for three parameters
public int add(int a, int b, int c) {
System.out.println("int add(int, int, int) called");
return a + b + c;
}
// Overload for arrays
public int add(int[] numbers) {
System.out.println("int add(int[]) called");
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}
// Overload with varargs
public int addVarargs(int... numbers) {
System.out.println("int addVarargs(int...) called");
return add(numbers);
}
// Overload for different object types
public String concatenate(String a, String b) {
System.out.println("String concatenate(String, String) called");
return a + b;
}
public String concatenate(String a, String b, String c) {
System.out.println("String concatenate(String, String, String) called");
return a + b + c;
}
}
// Overloading Demonstration
public void demonstrateOverloading() {
Calculator calc = new Calculator();
// Various overloads are called
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"));
}
}
Overloading with Inheritance
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");
}
// Overloaded, not overridden
public void makeSound(String intensity) {
System.out.println("Dog barks " + intensity);
}
// Additional overload
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
// Static binding for overloading (Compile-Time)
animal.makeSound(); // Animal makes a sound
animal.makeSound("loud"); // Animal makes a loud sound
dog.makeSound(); // Dog barks (overridden)
dog.makeSound("loud"); // Dog barks loud (overloaded)
dog.makeSound("loud", 3); // Dog barks loud (3x) (overloaded)
// Important: Static binding for overloading!
animalDog.makeSound(); // Dog barks (dynamic binding)
animalDog.makeSound("loud"); // Animal makes a loud sound (static binding!)
}
}
Generics and Parametric Polymorphism
Generic Classes
public class GenericPolymorphism {
// Generic Container class
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 method
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 with different types
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 with generic method
Container<Integer> lengthContainer = stringContainer.transform(String::length);
System.out.println("Length: " + lengthContainer);
// Generic Processor
Processor<String> stringProcessor = new Processor<>();
List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
// Filter
List<String> longWords = stringProcessor.filter(words, s -> s.length() > 5);
System.out.println("Long words: " + longWords);
// Map
List<Integer> lengths = stringProcessor.map(words, String::length);
System.out.println("Lengths: " + lengths);
// Reduce
String concatenated = stringProcessor.reduce(words, String::concat, "");
System.out.println("Concatenated: " + concatenated);
}
}
Generic Methods
public class GenericMethods {
// Generic method for comparison
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// Generic method for swap
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Generic method for conversion
public static <T, R> List<R> convertList(List<T> list, Function<T, R> converter) {
return list.stream()
.map(converter)
.collect(Collectors.toList());
}
// Generic method with 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 method
System.out.println("Max of 5 and 3: " + max(5, 3));
System.out.println("Max of 'Hello' and 'World': " + max("Hello", "World"));
// Swap method
String[] words = {"A", "B", "C"};
System.out.println("Before swap: " + Arrays.toString(words));
swap(words, 0, 2);
System.out.println("After swap: " + Arrays.toString(words));
// Convert method
List<String> strings = Arrays.asList("1", "2", "3", "4", "5");
List<Integer> integers = convertList(strings, Integer::parseInt);
System.out.println("Converted: " + integers);
// Wildcard methods
List<String> stringList = Arrays.asList("A", "B", "C");
List<Integer> intList = Arrays.asList(1, 2, 3);
System.out.println("String list:");
printList(stringList);
System.out.println("Integer list:");
printList(intList);
// Upper bounded wildcard
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("Sum: " + sumOfNumbers(doubles));
// Lower bounded wildcard
List<Number> numbers = new ArrayList<>();
addNumbers(numbers);
System.out.println("Numbers added: " + numbers);
}
}
Interfaces and Polymorphism
Interface-based Polymorphism
public class InterfacePolymorphism {
// Interface for polymorphic behavior
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();
}
// Class implements multiple 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("Drawing circle at (" + x + ", " + y + ") with radius " + radius);
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public String getType() {
return "Circle";
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
System.out.println("Circle moved to (" + 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("Drawing rectangle at (" + x + ", " + y + ") " + width + "x" + height);
}
@Override
public double getArea() {
return width * height;
}
@Override
public String getType() {
return "Rectangle";
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
System.out.println("Rectangle moved to (" + 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};
}
}
// Polymorphic processing
public void processShapes(List<Drawable> shapes) {
for (Drawable shape : shapes) {
shape.draw();
System.out.println("Type: " + shape.getType());
System.out.println("Area: " + 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("=== Drawing shapes ===");
processShapes(shapes);
System.out.println("=== Moving shapes ===");
moveShapes(movables, 10, 10);
System.out.println("=== After moving ===");
processShapes(shapes);
}
}
UML Notation for Polymorphism
Class Diagram Conventions
@startuml
' Polymorphic relationship 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 for polymorphic operations
interface Refundable {
+processRefund(amount: double): boolean
+getRefundStatus(): String
}
CreditCardProcessor ..|> Refundable
PayPalProcessor ..|> Refundable
BankTransferProcessor ..|> Refundable
@enduml
Sequence Diagram for Polymorphic Calls
@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 for Polymorphism
1. Liskov Substitution Principle
// Good: Rectangle can replace Shape anywhere
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;
}
// Additional methods do not violate 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. Interface Segregation
// Bad: Interface too large
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);
}
// Good: Specialized 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);
}
// Class implements only required interfaces
public class Circle implements Drawable, Movable, Resizable, Colored {
// Implementation...
}
3. Template Method Pattern
public abstract class DataProcessor {
// Template Method - defines algorithm
public final void processData() {
loadData();
if (validateData()) {
transformData();
saveData();
onSuccess();
} else {
onError();
}
cleanup();
}
// Abstract methods - must be implemented
protected abstract void loadData();
protected abstract boolean validateData();
protected abstract void transformData();
protected abstract void saveData();
// Hook methods - can be overridden
protected void onSuccess() {
System.out.println("Processing successful");
}
protected void onError() {
System.out.println("Processing failed");
}
protected void cleanup() {
System.out.println("Cleaning up");
}
}
public class CSVProcessor extends DataProcessor {
@Override
protected void loadData() {
System.out.println("Loading CSV data");
}
@Override
protected boolean validateData() {
System.out.println("Validating CSV data");
return true;
}
@Override
protected void transformData() {
System.out.println("Transforming CSV data");
}
@Override
protected void saveData() {
System.out.println("Saving CSV data");
}
@Override
protected void onSuccess() {
System.out.println("CSV processing successful!");
}
}
Exam-Relevant Concepts
Important Distinctions
-
Overriding vs Overloading
- Overriding: Same signature in subclass
- Overloading: Same name, different parameters
-
Static vs Dynamic Binding
- Static: Overloading (Compile-Time)
- Dynamic: Overriding (Runtime)
-
Abstract Class vs Interface
- Abstract Class: Common implementation
- Interface: Pure contract
-
Generics vs Inheritance
- Generics: Type safety at Compile-Time
- Inheritance: Polymorphism at Runtime
Typical Exam Tasks
- Draw UML diagrams for polymorphic relationships
- Implement overridden methods
- Explain dynamic binding
- Compare different polymorphism types
- Design polymorphic class hierarchies
Summary
Polymorphism is a powerful concept for flexible software architecture:
- Overriding enables dynamic binding and runtime polymorphism
- Overloading provides static binding and compile-time polymorphism
- Generics enable type-safe reusability
- Interfaces define polymorphic contracts
Good polymorphism requires adherence to the Liskov Substitution Principle and careful interface design for maintainable and extensible software.