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
// 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
// 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
// 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
// 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
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:
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.
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 aShape
).
javaShape myShape; // Superclass reference myShape = new Circle(5.0); // Points to a subclass object
Method Call:
- When you call an overridden method using the superclass reference:
javamyShape.draw();
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
). IfShape
doesn't have adraw()
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
'sdraw()
method is called, notShape
'sdraw()
method.
This decision of which method version to execute is made "dynamically" at runtime, based on the object's actual type.
- Compile-Time Check: The compiler checks if the method (e.g.,
In the example:
Shape myShape = new Circle(5.0);
myShape
is of typeShape
(compile-time type).- It refers to an object of type
Circle
(runtime type).
myShape.draw();
- Compiler checks: Does
Shape
have adraw()
method? Yes. - Runtime (JVM): What actual object is
myShape
pointing to? ACircle
. - JVM executes:
Circle
'sdraw()
method.
- Compiler checks: Does
myShape = new Rectangle(4.0, 6.0);
myShape
still typeShape
, but now refers toRectangle
.
myShape.draw();
- Compiler checks: Still fine.
- Runtime (JVM): Actual object is
Rectangle
. - JVM executes:
Rectangle
'sdraw()
method.
myShape = new Triangle(3.0, 7.0);
myShape.calculateArea();
- Compiler checks: Does
Shape
havecalculateArea()
? Yes. - Runtime (JVM): Actual object is
Triangle
. DoesTriangle
overridecalculateArea()
? No. - JVM executes:
Shape
'scalculateArea()
method (the closest one up the inheritance hierarchy).
- Compiler checks: Does
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 ofShape
objects, and the correctdraw()
andcalculateArea()
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
// --- 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()
}
}
--- 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
// ... (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();
}
}
}
--- 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!