Skip to main content

Section 10.4 Using Hierarchy in Parameters

In the previous section, we saw how the compiler understands the relationships defined by extends and implements. It knows that a Car contract includes the Vehicle contract, and a Circle contract includes the Drawable contract. This understanding enables a core concept in object-oriented programming called polymorphism. Simply put, polymorphism allows us to treat objects of different specific types in a uniform way, based on their shared contract (defined by a common superclass or interface). One of the most common places we leverage this is in method parameters.

Note 10.4.1. Key Term: Polymorphism.

Polymorphism (from Greek meaning "many forms") is a core object-oriented principle. In this context, it means that a single variable of a superclass or interface type can refer to objects of different specific subclass or implementing class types at runtime. This allows methods to treat these different objects uniformly based on their shared contract, leading to flexible and extensible code.

Subsection 10.4.1 Handling Subclasses via Superclass Parameters

Because a subclass contract includes the superclass contract, a method parameter declared with a superclass type can accept instances of any of its subclasses.
Imagine a method designed to process any kind of Vehicle:
 // Method that accepts any object fulfilling the Vehicle contract
public static void simulateMovement(Vehicle v, double timeStep) {
    // We can safely call methods defined in the Vehicle contract
    v.updatePosition(timeStep); // Assumes updatePosition is in Vehicle
    System.out.println(
        "Vehicle moved. Current speed: " + v.getSpeed() // Assumes getSpeed is in Vehicle
    );
    // We cannot call v.openTrunk() or v.ringBell() here,
    // because those are not part of the base Vehicle contract.
}

// We can call this method with different Vehicle subtypes
Car myCar = new Car(/*...*/);
Bicycle myBike = new Bicycle(/*...*/);
Motorcycle myMotorcycle = new Motorcycle(/*...*/);

simulateMovement(myCar, 0.1);        // Works! Car 'is-a' Vehicle
simulateMovement(myBike, 0.1);       // Works! Bicycle 'is-a' Vehicle
simulateMovement(myMotorcycle, 0.1); // Works! Motorcycle 'is-a' Vehicle
The simulateMovement method doesn’t need to know the specific type of vehicle (Car, Bicycle, etc.). It only relies on the guarantee that any object passed in will fulfill the Vehicle contract, allowing it to safely call methods like updatePosition and getSpeed. This significantly reduces code duplication – we write the simulation logic once, and it works for all current and future vehicle types that extend Vehicle.

Subsection 10.4.2 Handling Implementers via Interface Parameters

Similarly, a method parameter declared with an interface type can accept instances of any class that implements that interface.
Consider a method that can draw any object that knows how to draw itself, using the Drawable interface from the previous section:
 // Method that accepts any object fulfilling the Drawable contract
public static void renderOnScreen(Drawable item) {
    System.out.println("Preparing to draw...");
    // We can safely call methods defined in the Drawable contract
    item.draw(); // The actual draw() method executed depends on the runtime type of 'item'
    System.out.println("...drawing complete.");
    // We cannot call item.getRadius() or item.getWidth() here,
    // as those are specific to implementing classes, not the Drawable contract.
}

// Assume Circle, Rectangle, and FancyButton all implement Drawable
Circle circle = new Circle(/*...*/);
Rectangle rectangle = new Rectangle(/*...*/);
FancyButton button = new FancyButton(/*...*/); // Even UI elements can be Drawable

renderOnScreen(circle);    // Works! Circle implements Drawable
renderOnScreen(rectangle); // Works! Rectangle implements Drawable
renderOnScreen(button);    // Works! FancyButton implements Drawable
The renderOnScreen method works with completely unrelated types (Circle, Rectangle, FancyButton) as long as they fulfill the Drawable contract by implementing the draw method. This provides enormous flexibility, allowing different parts of a system to interact through shared capability contracts (interfaces) without needing to know about each other’s specific implementation details or inheritance structure.

Subsection 10.4.3 Polymorphism with Collections

This principle extends powerfully to collections. If you have a collection, such as an ArrayList, that holds references based on a superclass or interface type, you can store various related subtypes within it and process them uniformly.
// Assume ArrayList has been introduced
// Using List<Vehicle> ensures only Vehicle objects (or subclasses) can be added
java.util.List<Vehicle> traffic = new java.util.ArrayList<>();

traffic.add(new Car(...));
traffic.add(new Bicycle(...));
traffic.add(new Car(...));
traffic.add(new Motorcycle(...));

// Process all vehicles uniformly using the Vehicle contract
System.out.println("Updating all vehicle positions:");
for (Vehicle v : traffic) {
    // This works because every object in the list guarantees
    // it fulfills the Vehicle contract, which includes accelerate()
    v.accelerate(0.5); // Apply gentle acceleration to all
    simulateMovement(v, 0.5); // Reuse our previous method
}
Here, we can iterate through the list and call methods defined in the Vehicle contract on every element, regardless of whether it’s actually a Car, Bicycle, or Motorcycle object. This example also demonstrates dynamic dispatch: the actual implementation of methods like accelerate or updatePosition called at runtime will match the specific subclass of each object (e.g., the Car version for cars, the Bicycle version for bicycles, if they were overridden). Polymorphism thus combines compile-time safety (guaranteed contracts via the variable type Vehicle) with runtime flexibility (executing the behavior specific to the actual object type).

Note 10.4.2. Key Term: Dynamic Dispatch.

Dynamic dispatch (also known as late binding) is the mechanism that makes polymorphism work for overridden methods. When you call a method like v.accelerate(0.5) on a variable v of type Vehicle, the decision about which version of the accelerate method to run (the one in Vehicle, or an overridden one in Car, Motorcycle, etc.) is made at runtime, based on the actual class of the object v currently refers to.

Subsection 10.4.4 Benefits of Polymorphic Flexibility

Using superclass and interface types in method parameters provides significant benefits, leveraging the contracts enforced by the compiler:
  • Code Reuse: You can write a single method that operates on a whole family of related types (subclasses or implementing classes) instead of writing separate methods for each specific type.
  • Flexibility: Your methods can easily handle collections containing different subtypes, processing them based on their shared contract.
  • Extensibility: Adding new subclasses or implementing classes later (e.g., a new ElectricScooter extending Vehicle, or a UserProfileWidget implementing Drawable) is easier because existing polymorphic methods, like simulateMovement or renderOnScreen, typically require no modification at all to handle the new types, provided these new classes correctly implement the established contracts (Vehicle or Drawable respectively).
This polymorphic flexibility, enabled by Java’s type system and the contracts defined through inheritance and interfaces, is a powerful tool. However, as we’ll see in the next section, it also has limitations, particularly when we need type safety for unrelated types or want to avoid the potential risks and verbosity of casting. These limitations pave the way for understanding the necessity of Generics.
As we’ll see next, this flexibility is not unlimited-it introduces significant limitations when we require type safety across unrelated types or need to create reusable containers for arbitrary data types. These specific limitations directly motivated Java’s introduction of Generics, which we’ll explore after understanding the constraints of polymorphism alone.

Exercises 10.4.5 Exercises

1.

What is the primary concept that allows a single method like void process(Vehicle v) to accept objects of type Car, Bicycle, and Motorcycle (assuming they all extend Vehicle)?
  • Encapsulation
  • Incorrect. Encapsulation is about bundling data and methods within a class and controlling access, not about treating different types uniformly.
  • Type Casting
  • Incorrect. Casting might be needed to access specific subclass methods later, but polymorphism is what allows the method to *accept* the different subtypes initially based on the shared superclass contract.
  • Polymorphism
  • Correct. Polymorphism allows a variable or parameter of a superclass type (Vehicle) to refer to objects of its subclass types (Car, Bicycle), enabling uniform treatment based on the superclass contract.
  • Abstraction
  • Incorrect. While polymorphism relies on abstraction (the common contract defined by the superclass/interface), polymorphism specifically refers to the ability to treat different types through that common interface.

2.

When a method renderOnScreen(Drawable item) is called with a Circle object (where Circle implements Drawable), which version of the draw() method is executed?
  • The version defined in the Object class.
  • Incorrect. The Object class doesn’t define a draw method relevant here. Dynamic dispatch selects the method from the actual object’s class.
  • A default version possibly defined in the Drawable interface.
  • Incorrect. While interfaces *can* have default methods, polymorphism ensures that if the implementing class (Circle) provides its own version (@Override), that version is called.
  • It depends on how the renderOnScreen method calls it.
  • Incorrect. The calling method (renderOnScreen) doesn’t determine which override runs; the actual type of the object (item) at runtime does, via dynamic dispatch.
  • The version of draw() implemented specifically within the Circle class.
  • Correct. Due to dynamic dispatch, the JVM determines the actual type of the object referenced by item at runtime (which is Circle) and executes the draw() method belonging to that specific class.

3.

What is a major benefit of using polymorphism with collections, such as storing Car and Bicycle objects in a List<Vehicle>?
  • It allows you to call Car-specific methods directly on any element retrieved from the list without casting.
  • Incorrect. When retrieving elements as Vehicle, you can only directly call methods defined in the Vehicle contract without casting.
  • You can process all elements uniformly using a loop that treats each element as a Vehicle, calling methods defined in the Vehicle contract.
  • Correct. You can iterate through the collection and apply common operations defined in the superclass/interface type (Vehicle) to all elements, regardless of their specific subtype.
  • It automatically converts all objects in the list to the Vehicle type, losing subclass information.
  • Incorrect. The objects retain their specific types (Car, Bicycle) at runtime; only the reference variable used to access them in the loop has the more general type (Vehicle).
  • It ensures that only objects of the exact type Vehicle can be added, preventing subclasses.
  • Incorrect. A List<Vehicle> specifically *allows* instances of Vehicle and any of its subclasses to be added.
You have attempted of activities on this page.