Skip to content

Question 1

Provide a Java program where a superclass reference is used to call overridden methods in the subclass.

  • Explain how dynamic method dispatch is achieved in Java.
  • Extend the example to include method overriding and dynamic method dispatch.

Superclass – Shape

java
// Superclass
class Shape {
    // Method to be overridden
    public void draw() {
        System.out.println("Drawing a generic shape.");
    }

    // Method to be overridden
    public double calculateArea() {
        System.out.println("Cannot calculate area for a generic shape.");
        return 0.0;
    }

    // This method may or may not be overridden
    public void describe() {
        System.out.println("This is some kind of shape.");
    }
}

Subclass – Circle

java
// Subclass 1
class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a circle with radius: " + radius);
    }

    @Override
    public double calculateArea() {
        double area = Math.PI * radius * radius;
        System.out.println("Area of circle: " + area);
        return area;
    }

    // Circle-specific method (not part of Shape)
    public void roll() {
        System.out.println("Circle is rolling...");
    }
}

Subclass – Rectangle

java
// Subclass 2
class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a rectangle with width: " + width + " and height: " + height);
    }

    @Override
    public double calculateArea() {
        double area = width * height;
        System.out.println("Area of rectangle: " + area);
        return area;
    }

    @Override
    public void describe() {
        System.out.println("This is a four-sided rectangle.");
    }
}

Subclass – Triangle

java
// Subclass 3
class Triangle extends Shape {
    private double base;
    private double height;

    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a triangle with base: " + base + " and height: " + height);
    }

    // Does NOT override calculateArea() or describe()
}

Main Class – Demonstrating Dynamic Method Dispatch

java
public class DynamicDispatchDemo {
    public static void main(String[] args) {
        
        // --- 1. Shape reference to Shape object ---
        Shape genericShape = new Shape();
        System.out.println("--- Generic Shape ---");
        genericShape.draw();
        genericShape.calculateArea();
        genericShape.describe();
        System.out.println();

        // --- 2. Shape reference to Circle object ---
        Shape myShape = new Circle(5.0);
        System.out.println("--- Circle (via Shape reference) ---");
        myShape.draw();          // Calls Circle's draw()
        myShape.calculateArea(); // Calls Circle's calculateArea()
        myShape.describe();      // Inherited from Shape

        // Cannot call roll() directly on Shape reference
        if (myShape instanceof Circle) {
            ((Circle) myShape).roll(); // Downcasting to access subclass-specific method
        }
        System.out.println();

        // --- 3. Shape reference to Rectangle object ---
        myShape = new Rectangle(4.0, 6.0);
        System.out.println("--- Rectangle (via Shape reference) ---");
        myShape.draw();
        myShape.calculateArea();
        myShape.describe(); // Calls overridden method in Rectangle
        System.out.println();

        // --- 4. Shape reference to Triangle object ---
        myShape = new Triangle(3.0, 7.0);
        System.out.println("--- Triangle (via Shape reference) ---");
        myShape.draw();
        myShape.calculateArea(); // Calls Shape's version (not overridden)
        myShape.describe();      // Calls Shape's version
        System.out.println();

        // --- 5. Array of Shape references (Polymorphic processing) ---
        System.out.println("--- Array of Shapes ---");
        Shape[] shapes = new Shape[4];
        shapes[0] = new Circle(2.5);
        shapes[1] = new Rectangle(3, 5);
        shapes[2] = new Triangle(4, 2);
        shapes[3] = new Shape();

        for (Shape s : shapes) {
            System.out.print("Processing: ");
            s.draw();          // Dynamic method dispatch
            s.calculateArea(); // Overridden method called dynamically
            s.describe();      // Overridden if available, else default
            System.out.println("-----");
        }
    }
}

Dynamic Method Dispatch

Dynamic Method Dispatch (also known as Runtime Polymorphism or Late Binding) is a mechanism by which a call to an overridden method is resolved at runtime rather than at compile-time.

Here's how it's achieved in Java:

  1. Inheritance and Method Overriding:

    • You must have a superclass and at least one subclass.
    • The subclass must override one or more methods present in the superclass. An overridden method in the subclass has the same name, return type (or a covariant return type), and parameter list as the method in the superclass. The @Override annotation is good practice.
  2. Superclass Reference Variable:

    • You declare a reference variable of the superclass type.
    • This reference variable can then be made to point to an object of either the superclass itself or any of its subclasses. This is possible due to the "is-a" relationship (e.g., a Circle is a Shape).
    java
    Shape myShape; // Superclass reference
    myShape = new Circle(5.0); // Points to a subclass object
  3. Method Call:

    • When you call an overridden method using the superclass reference:
    java
    myShape.draw();
  4. Runtime Resolution (The "Dynamic" Part):

    • Compile-Time Check: The compiler checks if the method (e.g., draw()) exists in the declared type of the reference variable (Shape). If Shape doesn't have a draw() method (or an inherited one), you'll get a compile-time error.
    • Runtime Execution: At runtime, the Java Virtual Machine (JVM) looks at the actual object type that the reference variable is currently pointing to (in this case, a Circle object).
    • The JVM then executes the version of the method that belongs to that actual object's class. So, Circle's draw() method is called, not Shape's draw() method.

    This decision of which method version to execute is made "dynamically" at runtime, based on the object's actual type.

In the example:

  • Shape myShape = new Circle(5.0);
    • myShape is of type Shape (compile-time type).
    • It refers to an object of type Circle (runtime type).
  • myShape.draw();
    • Compiler checks: Does Shape have a draw() method? Yes.
    • Runtime (JVM): What actual object is myShape pointing to? A Circle.
    • JVM executes: Circle's draw() method.
  • myShape = new Rectangle(4.0, 6.0);
    • myShape still type Shape, but now refers to Rectangle.
  • myShape.draw();
    • Compiler checks: Still fine.
    • Runtime (JVM): Actual object is Rectangle.
    • JVM executes: Rectangle's draw() method.
  • myShape = new Triangle(3.0, 7.0);
  • myShape.calculateArea();
    • Compiler checks: Does Shape have calculateArea()? Yes.
    • Runtime (JVM): Actual object is Triangle. Does Triangle override calculateArea()? No.
    • JVM executes: Shape's calculateArea() method (the closest one up the inheritance hierarchy).

Use of Dynamic Method

  • Flexibility and Extensibility: You can write generic code that operates on superclass types, and it will automatically work correctly with any new subclasses created later, as long as they adhere to the superclass's contract (i.e., override methods appropriately).

  • Polymorphism: It allows you to treat objects of different classes that share a common superclass in a uniform way. The shapes array in the example perfectly illustrates this. You can loop through an array of Shape objects, and the correct draw() and calculateArea() methods are called for each specific shape.

  • Adherence to Liskov Substitution Principle: Subtypes should be substitutable for their base types without altering the correctness of the program. Dynamic dispatch is a key enabler of this.


Vehicle Example

java
// --- 1. Superclass ---
class Vehicle {
    // Method in the superclass
    public void startEngine() {
        System.out.println("Vehicle: Making a generic engine sound.");
    }

    // Another method in the superclass
    public void stop() {
        System.out.println("Vehicle: Stopping the vehicle.");
    }
}

// --- 2. Subclass 1 ---
class Car extends Vehicle {
    // Overriding the startEngine method from the superclass
    @Override
    public void startEngine() {
        System.out.println("Car: Turning the key... Vroom vroom!");
    }

    // A method specific only to Car
    public void openTrunk() {
        System.out.println("Car: Trunk opened.");
    }
}

// --- 3. Subclass 2 ---
class Motorcycle extends Vehicle {
    // Overriding the startEngine method from the superclass
    @Override
    public void startEngine() {
        System.out.println("Motorcycle: Kick starting... Brrrrm!");
    }
}

// --- 4. Main Class for Demonstration ---
public class DynamicDispatchDemo {
    public static void main(String[] args) {

        // Create a reference variable of the superclass type (Vehicle)
        Vehicle myVehicleRef;

        // --- Scenario 1: Reference points to a Superclass object ---
        System.out.println("--- Scenario 1: Vehicle ref -> Vehicle object ---");
        myVehicleRef = new Vehicle(); // Object is Vehicle
        // Call the method using the superclass reference
        myVehicleRef.startEngine(); // Calls Vehicle's startEngine()
        myVehicleRef.stop();        // Calls Vehicle's stop()

        System.out.println("\n--- Scenario 2: Vehicle ref -> Car object ---");
        // --- Scenario 2: Reference points to a Subclass object (Car) ---
        // This is where polymorphism is possible.
        myVehicleRef = new Car(); // Object is Car

        // DYNAMIC METHOD DISPATCH happens here!
        // Even though the reference type is 'Vehicle', the method executed
        // is the one belonging to the actual object, which is 'Car'.
        myVehicleRef.startEngine(); // Calls Car's startEngine()

        myVehicleRef.stop();        // Calls Vehicle's stop() (since Car didn't override it)

        // Note: You cannot call methods specific to the subclass using a superclass reference.
        // myVehicleRef.openTrunk(); 
        // ERROR: Compile-time error! The Vehicle class doesn't know openTrunk().

        System.out.println("\n--- Scenario 3: Vehicle ref -> Motorcycle object ---");
        // --- Scenario 3: Reference points to another Subclass object (Motorcycle) ---
        myVehicleRef = new Motorcycle(); // Object is Motorcycle

        // DYNAMIC METHOD DISPATCH happens here again!
        // The JVM now selects the method from the 'Motorcycle' object.
        myVehicleRef.startEngine(); // Calls Motorcycle's startEngine()
        myVehicleRef.stop();        // Calls Vehicle's stop()
    }
}
bash
--- Scenario 1: Vehicle ref -> Vehicle object ---
Vehicle: Making a generic engine sound.
Vehicle: Stopping the vehicle.

--- Scenario 2: Vehicle ref -> Car object ---
Car: Turning the key... Vroom vroom!
Vehicle: Stopping the vehicle.

--- Scenario 3: Vehicle ref -> Motorcycle object ---
Motorcycle: Kick starting... Brrrrm!
Vehicle: Stopping the vehicle.

Application of Dynamic Method Dispatch

java
// ... (Vehicle, Car, and Motorcycle classes remain the same)

public class DynamicDispatchExtendedDemo {
    public static void main(String[] args) {

        // Create an array of type Vehicle
        Vehicle[] garage = new Vehicle[4];

        // Populate the array with different types of objects
        garage[0] = new Car();
        garage[1] = new Motorcycle();
        garage[2] = new Vehicle(); // A generic vehicle
        garage[3] = new Car();     // Another car

        System.out.println("--- Testing all vehicles in the garage ---");

        // Iterate through the array
        for (Vehicle v : garage) {
            // DYNAMIC METHOD DISPATCH IN ACTION!
            // Each time through the loop, the JVM looks at the actual object (Car, Motorcycle, or Vehicle)
            // and calls the appropriate startEngine() method.
            v.startEngine();
        }
    }
}
bash
--- Testing all vehicles in the garage ---
Car: Turning the key... Vroom vroom!
Motorcycle: Kick starting... Brrrrm!
Vehicle: Making a generic engine sound.
Car: Turning the key... Vroom vroom!

Made with ❤️ for students, by a fellow learner.