Skip to content

Java Polymorphism

Polymorphism, meaning “many forms”, is a core OOP concept. It allows objects of different classes to be treated as objects of a common superclass or interface. The most common use is when a superclass reference variable refers to an object of one of its subclasses.

An object is polymorphic if it passes more than one IS-A test (determined by inheritance and interface implementation). In Java, every object is inherently polymorphic because it IS-A itself and it IS-A Object.

Key points to remember:

  • Objects are accessed via reference variables.
  • A reference variable has a declared type (class or interface) that cannot change.
  • The type of the reference variable determines which methods can be called at compile time.
  • A reference variable can refer to an object of its declared type or any subtype (subclass or implementing class) of its declared type.
  • A single object in memory can be referred to by multiple reference variables of different compatible types.

Consider these definitions:

interface Vegetarian {}
class Animal {}
class Deer extends Animal implements Vegetarian {}

A Deer object exhibits polymorphism:

  • A Deer IS-A Animal (due to extends)
  • A Deer IS-A Vegetarian (due to implements)
  • A Deer IS-A Deer (its own type)
  • A Deer IS-A Object (implicitly)

Therefore, the following assignments are valid:

// Create a Deer object
Deer deerObj = new Deer();
// Assign the Deer object to references of its supertypes
Animal animalRef = deerObj; // OK: Deer IS-A Animal
Vegetarian vegetarianRef = deerObj; // OK: Deer IS-A Vegetarian
Object objectRef = deerObj; // OK: Deer IS-A Object
Deer deerRef = deerObj; // OK: Deer IS-A Deer
// All references (deerRef, animalRef, vegetarianRef, objectRef) point to the SAME Deer object in memory.

Polymorphism shines when combined with method overriding. When a subclass provides its own specific implementation of a method inherited from a superclass, it overrides the superclass method.

Java uses virtual method invocation (also called dynamic method dispatch or late binding). This means:

  • Compile Time: The compiler checks if a method call is valid based on the reference variable’s type. (e.g., animalRef.eat() is valid only if the Animal class has an eat() method).
  • Run Time: The JVM determines which actual method implementation to execute based on the actual object’s type that the reference points to at that moment. If the actual object is a Deer, the Deer’s version of the method is called, even if the reference is of type Animal.

All non-static, non-final, non-private methods in Java are implicitly virtual.

Consider an Employee superclass and a Salary subclass that overrides the mailCheck method.

/* File: Employee.java */
public class Employee {
private String name;
private String address;
private int number;
public Employee(String name, String address, int number) {
System.out.println("Constructing an Employee");
this.name = name;
this.address = address;
this.number = number;
}
// Base mailCheck implementation
public void mailCheck() {
System.out.println("Mailing check to " + this.name + " at " + this.address);
}
@Override
public String toString() {
return name + " " + address + " " + number;
}
// Getters and Setters omitted for brevity...
public String getName() { return name; }
public String getAddress() { return address; }
public void setAddress(String newAddress) { address = newAddress; }
public int getNumber() { return number; }
}
/* File: Salary.java */
public class Salary extends Employee {
private double salary; // Annual salary
public Salary(String name, String address, int number, double salary) {
super(name, address, number); // Call Employee constructor
setSalary(salary);
}
// Overridden mailCheck implementation
@Override
public void mailCheck() {
System.out.println("Within Salary's mailCheck method:");
System.out.println("Mailing check to " + getName() + " (Salary: " + salary + ") at " + getAddress());
}
public double getSalary() {
return salary;
}
public void setSalary(double newSalary) {
if (newSalary >= 0.0) {
salary = newSalary;
} else {
System.err.println("Salary cannot be negative.");
}
}
public double computePay() {
System.out.println("Computing salary pay for " + getName());
return salary / 52; // Weekly pay
}
}

Demonstrating Polymorphic Behavior:

/* File: VirtualDemo.java */
public class VirtualDemo {
public static void main(String[] args) {
// Reference and object are both Salary
Salary salRef = new Salary("Mohd Mohtashim", "Ambehta, UP", 3, 52000.00);
// Reference is Employee, but object is Salary (Polymorphism)
Employee empRef = new Salary("John Adams", "Boston, MA", 2, 60000.00);
// Alternative using the previous Salary object:
// Employee empRef = salRef;
System.out.println("Calling mailCheck using Salary reference:");
salRef.mailCheck(); // Calls Salary's mailCheck()
System.out.println("\nCalling mailCheck using Employee reference:");
empRef.mailCheck(); // Calls Salary's mailCheck() due to runtime object type!
// Compiler error if uncommented: computePay() is not in Employee class
// empRef.computePay();
// To call Salary-specific methods via empRef, explicit casting is needed:
if (empRef instanceof Salary) {
Salary specificSalRef = (Salary) empRef;
double weeklyPay = specificSalRef.computePay();
System.out.println("\nJohn Adams weekly pay: " + weeklyPay);
}
}
}

Output:

Constructing an Employee
Constructing an Employee
Calling mailCheck using Salary reference:
Within Salary's mailCheck method:
Mailing check to Mohd Mohtashim (Salary: 52000.0) at Ambehta, UP
Calling mailCheck using Employee reference:
Within Salary's mailCheck method:
Mailing check to John Adams (Salary: 60000.0) at Boston, MA
Computing salary pay for John Adams
John Adams weekly pay: 1153.846...

Explanation:

  • When salRef.mailCheck() is called, both the compile-time type (Salary) and runtime type (Salary) have mailCheck(). The Salary version is executed.
  • When empRef.mailCheck() is called:
    • At compile time, the compiler checks the Employee class for mailCheck(). It finds it, so the code compiles.
    • At run time, the JVM sees that empRef actually points to a Salary object. Therefore, it executes the overridden mailCheck() method from the Salary class.
  • This runtime selection of the appropriate overridden method is the essence of virtual method invocation and polymorphism in action.
  • Calling methods specific to the subclass (like computePay) requires checking the type with instanceof and casting the reference variable.

Polymorphism enables writing flexible and extensible code. You can write methods that operate on superclass or interface types, and they will correctly work with any current or future subclass objects passed to them, invoking the appropriate specialized behavior.