Skip to content
IRC-Coding IRC-Coding
UML Polymorphism Dynamic Binding Method Overriding Overloading Generics OOP Inheritance

UML Polymorphism Basics: Dynamic Binding & Overriding

Learn UML polymorphism with dynamic binding, method overriding, overloading, and generics. Java examples included.

S

schutzgeist

2 min read

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

  1. Overriding (Override): Method in subclass replaces base class method
  2. Overloading (Overload): Multiple methods with the same name but different parameters
  3. Parametric Polymorphism: Generics for type-safe reusability
  4. 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

  1. Overriding vs Overloading

    • Overriding: Same signature in subclass
    • Overloading: Same name, different parameters
  2. Static vs Dynamic Binding

    • Static: Overloading (Compile-Time)
    • Dynamic: Overriding (Runtime)
  3. Abstract Class vs Interface

    • Abstract Class: Common implementation
    • Interface: Pure contract
  4. Generics vs Inheritance

    • Generics: Type safety at Compile-Time
    • Inheritance: Polymorphism at Runtime

Typical Exam Tasks

  1. Draw UML diagrams for polymorphic relationships
  2. Implement overridden methods
  3. Explain dynamic binding
  4. Compare different polymorphism types
  5. 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.

Back to Blog
Share:

Related Posts