Polymorphism 2 – Object-Oriented Programming

In the for(:) loop at (3) in Example 5.23, depending on the type of the object denoted by the loop variable drawable, the call to the draw() method will result in the draw() method of this object to be executed.

The instanceof type comparison operator returns true if the reference specified as its left-hand operand at runtime denotes an object whose type is the same as or is a subtype of the reference type specified in its right-hand operand. If the types of the operands are unrelated, the compiler issues an error.

Click here to view code image

IDrawable d1 = new Rectangle();
System.out.println(d1 instanceof IDrawable); // true. Rectangle is an IDrawable.
System.out.println(d1 instanceof Shape);     // true. Rectangle is a Shape.
System.out.println(d1 instanceof Rectangle); // true. Rectangle is a Rectangle.
System.out.println(d1 instanceof Circle);    // false. Rectangle is not a Circle.
System.out.println(d1 instanceof Graph);     // false. Rectangle is not a Graph.
// System.out.println(d1 instanceof String); // Unrelated. Compile-time error.

In Example 5.23, the abstract method area() of the abstract class Shape is implemented by all subclasses of the superclass Shape. The code below does not compile, as the compiler determines that the method area() is not defined by the interface IDrawable.

Click here to view code image

IDrawable d2 = new Square();  // Subtype object denoted by supertype reference.
d2.area();        // Method not defined for type IDrawable. Compile-time error!

In order to elicit subtype-specific behavior in an object that is denoted by a super-type reference, the reference must be cast to the subtype. The cast is specified as (reference_type). The cast in this case applies a narrowing reference conversion from a supertype to a subtype. The cast appeases the compiler. At runtime, the type of the object still determines which area() method will be executed.

Click here to view code image

((Square) d2).area();         // Prints “Computing area of a Square.”
((Shape) d2).area();          // Prints “Computing area of a Square.”

The code above executes normally because the reference d2 at runtime denotes an object whose type defines the appropriate area() method. The code below shows that the cast alone is not enough to guarantee that the execution will proceed normally, even though there was no compile-time error. The Graph object denoted by the reference d3 at runtime does not define the area() method.

Click here to view code image

IDrawable d3 = new Graph();       // No compile-time error.
((Shape) d3).area();              // Throws a ClassCastException!

Guaranteeing the correct subtype and casting to a subtype reference safely can be accomplished using the instanceof pattern match operator.

Click here to view code image

if (d2 instanceof Shape shape) {           // true
  shape.area();                            // Prints “Computing area of a Square.”
} else {
  System.out.println(d2.getClass().getName() + ” is not a Shape.” );
}
if (d3 instanceof Shape shape) {           // false
  shape.area();
} else {
  System.out.println(d3.getClass().getName() +
                     ” is not a Shape.” ); // Prints “Graph is not a Shape.”
}

In the for(:) loop at (4) in Example 5.23, we are only interested in drawing objects from the drawables array that are of type Shape. The binary instanceof type comparison operator in the conditional of the if statement at (5) is used to determine whether the type of an object in the drawables array is of type Shape. The draw() method is only called on objects that satisfy this condition.

The for(:) loop at (6) in Example 5.23 uses the instanceof pattern match operator at (7) to select those IDrawable objects whose type is Shape, and invoke the area() method on them.

A private instance method does not exhibit polymorphic behavior. A call to such a method can occur only within the class and gets bound to the private method implementation at compile time.

Overloaded instance methods do not exhibit polymorphic behavior, as their calls are bound at compile time, unless an overloaded method is also overridden and invoked by a supertype reference.

Static methods also do not exhibit polymorphic behavior, as these methods do not belong to objects.

Polymorphism is achieved through inheritance and interface implementation. Code relying on polymorphic behavior will still work without any change if new subclasses or new classes implementing the interface are added. If no obvious is-a relationship is present, polymorphism is best achieved by using aggregation with interface implementation.

Polymorphism and dynamic method lookup form a powerful programming paradigm that simplifies client definitions, encourages object decoupling, and supports dynamically changing relationships between objects at runtime.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *