UML Polymorphism Fundamentals: Dynamic Binding & Method Overriding
Polymorphism is a cornerstone concept in object-oriented programming and UML modeling. It allows objects from different classes to respond to the same message in different ways.
What is Polymorphism?
Polymorphism (literally “many forms”) describes the ability of objects to share the same interface while having different implementations. In UML, this is represented through inheritance hierarchies and interfaces.
Types of Polymorphism
- Overriding: A subclass method replaces the base class method
- Overloading: Multiple methods with the same name but different parameters
- Parametric Polymorphism: Generics for type-safe code reuse
- 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 the object's actual type
double area = shape.area(); // Calls Rectangle.area() or Circle.area()
double perimeter = shape.perimeter(); // Calls 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();
// Different overloads are invoked
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 applies to 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<>("Number", 42);
Container<List<String>> listContainer = new Container<>("List",
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");
// Filtering
List<String> longWords = stringProcessor.filter(words, s -> s.length() > 5);
System.out.println("Long words: " + longWords);
// Mapping
List<Integer> lengths = stringProcessor.map(words, String::length);
System.out.println("Lengths: " + lengths);
// Reducing
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 broad
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 the interfaces it needs
public class Circle implements Drawable, Movable, Resizable, Colored {
// Implementation...
}
3. Template Method Pattern
public abstract class DataProcessor {
// Template Method - defines the 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
Key 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: Shared implementation
- Interface: Pure contract
-
Generics vs Inheritance
- Generics: Type safety at compile-time
- Inheritance: Polymorphism at runtime
Typical Exam Questions
- 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 reuse
- Interfaces define polymorphic contracts
Good polymorphism requires adherence to the Liskov Substitution Principle and careful interface design for maintainable and extensible software.
Next in the UML Learning Path
The next article in the UML learning path covers UML Class Diagrams: Relationships - Association, Aggregation, Composition, Dependency, Inheritance — the details on different relationship types in class diagrams.

