Which object a reference will actually denote during runtime, cannot always be determined at compile time. Polymorphism allows a reference to denote objects of different types at different times during execution. A supertype reference exhibits polymorphic behavior, since it can denote objects of its subtypes.
When a non-private instance method is invoked on an object, the method definition actually executed is determined both by the type of the object at runtime and the method signature. Dynamic method lookup is the process of determining which method definition a method signature denotes during runtime, based on the type of the object. However, a call to a private instance method is not polymorphic. Such a call can only occur within the class, and gets bound to the private method implementation at compile time.
The inheritance hierarchy depicted in Figure 6.5 is implemented in Example 6.14. The implementation of the method draw() is overridden in all subclasses of the class Shape. The invocation of the draw() method in the two loops at (3) and (4) in Example 6.14, relies on the polymorphic behavior of references and dynamic method lookup. The array shapes holds Shape references denoting a Circle, a Rectangle and a Square, as shown at (1). At runtime, dynamic lookup determines the draw() implementation to execute, based on the type of the object denoted by each element in the array. This is also the case for the elements of the array drawables at (2), which holds IDrawable references that can be assigned any object of a class that implements the IDrawable interface. The first loop will still work without any change if objects of new subclasses of the class Shape are added to the array shapes. If they did not override the draw() method, then an inherited version of the method would be executed. This polymorphic behavior applies to the array drawables, where the subtype objects are guaranteed to have implemented the IDrawable interface.
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.
interface IDrawable { void draw(); } class Shape implements IDrawable { public void draw() { System.out.println("Drawing a Shape."); } } class Circle extends Shape { public void draw() { System.out.println("Drawing a Circle."); } } class Rectangle extends Shape { public void draw() { System.out.println("Drawing a Rectangle."); } } class Square extends Rectangle { public void draw() { System.out.println("Drawing a Square."); } } class Map implements IDrawable { public void draw() { System.out.println("Drawing a Map."); } } public class PolymorphRefs { public static void main(String[] args) { Shape[] shapes = {new Circle(), new Rectangle(), new Square()}; // (1) IDrawable[] drawables = {new Shape(), new Rectangle(), new Map()};// (2) System.out.println("Draw shapes:"); for (int i = 0; i < shapes.length; i++) // (3) shapes[i].draw(); System.out.println("Draw drawables:"); for (int i = 0; i < drawables.length; i++) // (4) drawables[i].draw(); } }
Output from the program:
Draw shapes: Drawing a Circle. Drawing a Rectangle. Drawing a Square. Draw drawables: Drawing a Shape. Drawing a Rectangle. Drawing a Map.