OCA Java SE 8 Class Design - Java Polymorphism








Java supports polymorphism, the property of an object to take on many different forms.

A Java object may be accessed using a reference with the same type as the object.

The reference can be a superclass of the object, or a reference that defines an interface the object implements, either directly or through a superclass.

A cast is not required if the object is being reassigned to a super type or interface of the object.

public class Shape { 
  public boolean hasEdge() { 
    return true; 
  } 
} 

public interface Printable { 
  public boolean isReady(); 
} 

public class Rectangle extends Shape implements Printable { 
  public boolean isReady() { 
     return false; 
  } 
  public int edge = 10; 
  public static void main(String[] args) { 
     Rectangle r = new Rectangle(); 
     System.out.println(r.edge); 

     Printable hasTail = r; 
     System.out.println(hasTail.isReady()); 

     Shape primate = r; 
     System.out.println(primate.hasEdge()); 
  } 
} 

An instance of Rectangle can be passed as an instance of an interface it implements, Printable, as well as an instance of one of its superclasses, Shape. This is the nature of polymorphism.

Once the object has been assigned a new reference type, only the methods and variables available to that reference type are callable without an explicit cast.

For example, the following snippets of code will not compile:

    Printable h = r; 
    System.out.println(h.edge);  // DOES NOT COMPILE 

    Shape p = r; 
    System.out.println(p.isReady());  // DOES NOT COMPILE 

In the code above h has direct access only to methods defined with the Printable interface; therefore, it doesn't know the variable edge is part of the object.

The reference p has access only to methods defined in the Shape class, and it doesn't have direct access to the isReady() method.





Casting Objects

We can reclaim those references by casting the object back to the specific subclass it came from:

Shape p = r; 
Rectangle r2 = p; // DOES NOT COMPILE 

Rectangle r3 = (Rectangle)p; 
System.out.println(r3.edge); 

In this example, we first try to convert the p reference back to a r reference, r2, without an explicit cast.

The code will not compile.

In the second example, we explicitly cast the object to a subclass of the object Shape and we gain access to all the methods available to the Rectangle class.

Here are some basic rules when casting variables:

Casting an object from a subclass to a superclass doesn't require an explicit cast.

Casting an object from a superclass to a subclass requires an explicit cast.

We canno cast to unrelated types.

Even when the code compiles without issue, an exception may be thrown at runtime if the object being cast is not an instance of that class.

Consider this example:

public class Person {} 

public class Shape { 
  public static void main(String[] args) { 
    Shape s = new Shape(); 
    Person p = (Person)s;  // DOES NOT COMPILE 
  } 
} 

In this example, the classes Shape and Person are not related through any class hierarchy.

Even though two classes share a related hierarchy, that doesn't mean an instance of one can automatically be cast to another.

Here's an example:

public class Shape { 
} 

public class Rectangle extends Shape { 
  public static void main(String[] args) { 
    Shape s = new Shape(); 
    Rectangle capybara = (Rectangle)s; // Throws ClassCastException at runtime 
  } 
} 

This code creates an instance of Shape and then tries to cast it to a subclass of Shape, Rectangle.

Although this code will compile without issue, it will throw a ClassCastException at runtime since the object being referenced is not an instance of the Rectangle class.





Virtual Methods

A virtual method is a method in which the specific implementation is not determined until runtime.

All non-final, non-static, and non-private Java methods are considered virtual methods, since any of them can be overridden at runtime.

If you call a method on an object that overrides a method, you get the overridden method, even if the call to the method is on a parent reference or within the parent class.

We'll illustrate this principle with the following example:

class Person { 
  public String getName() { 
    return "Unknown"; 
  } 
  public void displayInformation() { 
    System.out.println("The name is: "+getName()); 
  } 
} 

class Employee extends Person { 
  public String getName() { 
    return "Employee"; 
  } 
  public static void main(String[] args) { 
    Person bird = new Employee(); 
    bird.displayInformation(); 
  } 
}  

This code compiles and executes without issue and outputs the following:

The name is: Employee 

The method getName() is overridden in the child class Employee. The value of the getName() method at runtime in the displayInformation() method is replaced with the value of the implementation in the subclass Employee.

Even though the parent class Person defines its own version of getName() and doesn't know anything about the Employee class during compile-time, at runtime the instance uses the overridden version of the method, as defined on the instance of the object.

Polymorphic Parameters

With polymorphism we can pass instances of a subclass or interface to a method.

For example, you can define a method that takes an instance of an interface as a parameter. In this manner, any class that implements the interface can be passed to the method.

Since you're casting from a subtype to a supertype, an explicit cast is not required.

This property is referred to as polymorphic parameters of a method.

class Shape { /*from  w  ww .j a va2 s.c o m*/
  public String getName() { 
    return "Shape"; 
  } 
} 

class Circle extends Shape { 
  public String getName() { 
    return "Circle"; 
  } 
} 

class Rectangle extends Shape { 
  public String getName() { 
    return "Rectangle"; 
  } 
} 

public class Main { 
  public static void feed(Shape shape) { 
    System.out.println("Feeding shape "+shape.getName()); 
  } 
  public static void main(String[] args) { 
    feed(new Circle()); 
    feed(new Rectangle()); 
    feed(new Shape()); 
  } 
} 

feed(Shape shape) method in this example can handle instances of Circle and Rectangle without issue, because both are subclasses of the Shape class.

The code above generates the following result.