Polymorphism is one of the fundamental principles of Object-Oriented Software. The term typically means that something that can have multiple forms. In object-oriented methodology, polymorphism enables writing programs that have late binding references. Although creating polymorphic reference in Java is easy, the concept behind it has a deeper impact on overall programming. This article explores some of the intricate details about polymorphism and its implication on object-oriented programming.
Polymorphic Reference: An Overview
A polymorphic reference is a variable that can refer to different types of objects at different points in time. It is typically compatible with the class that it refers to. For example, in the following case:
Employee employee;
The ‘employee‘ is a reference variable that may refer to an instance of the Employee class. The qualification of the reference variable to a referred object is determined by its compatibility. This may seem to be the only reliable condition, but that is not exactly true, especially when implementing polymorphism. The rule is too rigid, but polymorphism makes is more flexible by incorporating the idea of “having many forms.” This means that a polymorphic reference guarantees that it can refer to different types of objects at different points in time rather than being stuck with the idea of an exact match for compatibility. Therefore, if a reference can be used to invoke a method at one point in time, it can be dynamically changed to point to another object and invoke some other methods the next time. This leverages flexibility by giving another dimension of use of the reference variable.
When a reference variable is bound to an object that cannot be changed at runtime, or, in other words, the binding of method invocation to a method definition is done at compile time, it is called static binding. If the binding is changeable at runtime, as in the case of polymorphic references where the decision of binding is made only during execution, it is called dynamic binding or late binding. Both have their uses in object-oriented programming and it is not that one outweighs the other. However, the deferred commitment of binding in the case of a polymorphic reference gives it an edge over compile time binding in terms of flexibility, but, on the other hand, compromises on the overhead of performance. But, this is acceptable to a large extent and the overhead popularly has a very minor appeal with respect to augmenting the efficiency.
Creating Polymorphism
In Java, polymorphic reference can be created in two ways: either using inheritance or using interfaces.
Polymorphism by Inheritance
A reference variable refers to an instance of a class. In the case of inheritance hierarchy, a reference object may point to an instance of any classes in the hierarchy, provided the reference variable is declared as the parent class type in the hierarchical tree. This means that, in Java, because the Object class is the parent class or super class of all the classes or, in other words, all classes in Java are, in fact, subclasses of the Object class implicitly or explicitly, a reference variable of the Object type can refer to any class instance in Java. Here’s what we mean.
Employee employee; Object object; employee = new Employee(); object = employee; // This is a valid assignment
If the situation is reversed, such as follows:
Employee employee; Object object = new Object(); employee = (Employee)object // Valid, but needs explicit cast
Observe that it requires an explicit cast; only then it becomes a valid statement. As can be seen, such a reverse assignment is less useful to the verge of being problematic on many occasions. This is because the functionality of the Object instance has very little to do with the functionality expected from the Employee reference variable. The relation is-a can be derived from the employee-is-an-object derivative; the reverse relationship in this case, such as, an-object-is-an-employee is just too far-fetched.
Polymorphism by Inheritance: An Example
Let’s try to understand it with the help of an example.
Figure 1: Classes derived from driver class, Company
The driver class, called the Company, creates a list of employees and invokes the paySalary() method. The Payroll class maintains a list of different types of employees in the company. Note that the array is declared as the array of reference variable derived from the Employee class, which is the parent or super class of all the employee subclasses. As a result, the array can be filled with object references created from any of the subclasses of the Employee class, such as CommissionEmployee, HourlyEmployee, or SalariedEmployee. In the paySalary() definition, the appropriate salary() method is invoked according to the object reference in the array. The invocation of the salary() method is thus polymorphic, as evident, each class has its own version of the salary() method.
The employees array in the Payroll class does not represent a specific type of Employee. It serves as a handle that can point any type of Employee subclass reference. Although the inherited classes share some common data, inherited as descendants, they are distinct with their own set of properties.
Polymorphism by Inheritance: Java Implementation
Here is the quick implementation of the example in Java.
package org.mano.example; public class Company { public static void main( String[] args ) { Payroll payroll = new Payroll(); payroll.paySalary(); } } package org.mano.example; import java.util.ArrayList; import java.util.List; public class Payroll { private List<Employee> employees = new ArrayList<>(); public Payroll() { employees.add(new SalariedEmployee("Harry Potter", "123-234-345",7800)); employees.add(new CommissionEmployee("Peter Parker", "234-345-456",2345.67,0.15)); employees.add(new HourlyEmployee("Joker Poker", "456-567-678",562.36,239.88)); } public void paySalary() { for (Employee e: employees) { System.out.println ("----------------------------------------------------"); System.out.println(e.toString()); System.out.printf ("Gross payment: $%,.2fn",e.salary()); System.out.println ("----------------------------------------------------"); } } } package org.mano.example; public abstract class Employee { protected String name; protected String ssn; public Employee(String name, String ssn) { this.name = name; this.ssn = ssn; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSsn() { return ssn; } public void setSsn(String ssn) { this.ssn = ssn; } @Override public String toString() { return String.format("%snSSN: %s", getName(),getSsn()); } public abstract double salary(); } package org.mano.example; public class SalariedEmployee extends Employee { protected double basicSalary; public SalariedEmployee(String name, String ssn, double basicSalary) { super(name, ssn); setBasicSalary(basicSalary); } public double getBasicSalary() { return basicSalary; } public void setBasicSalary(double basicSalary) { if(basicSalary>= 0.0) this.basicSalary = basicSalary; else throw new IllegalArgumentException("basic " + "salary must be greater than 0.0"); } @Override public double salary() { eturn getBasicSalary(); } @Override public String toString() { return String.format("%snBasic Salary: $%,.2f", super.toString(),getBasicSalary()); } } package org.mano.example; public class HourlyEmployee extends Employee { protected double wage; protected double hours; public HourlyEmployee(String name, String ssn, double wage, double hours) { super (name, ssn); setWage(wage); setHours(hours); } public double getWage() { return wage; } public void setWage(double wage) { if(wage >= 0.0) this.wage = wage; else throw new IllegalArgumentException("wage " + "must be > 0.0"); } public double getHours() { return hours; } public void setHours(double hours) { if(hours >= 0.0) this.hours = hours; else throw new IllegalArgumentException("hours " + "must be > 0.0"); } @Override public double salary() { return getHours() * getWage(); } @Override public String toString() { return String.format("%snWage: $%, .2fnHours worked: %,.2f", super.toString(),getWage(),getHours()); } } package org.mano.example; public class CommissionEmployee extends Employee { protected double sales; protected double commission; public CommissionEmployee(String name, String ssn, double sales, double commission) { super(name, ssn); setSales(sales); setCommission(commission); } public double getSales() { return sales; } public void setSales(double sales) { if(sales >=0.0) this.sales = sales; else throw new IllegalArgumentException("Sales " + "must be >= 0.0"); } public double getCommission() { return commission; } public void setCommission(double commission) { if(commission > 0.0 && commission < 1.0) this.commission = commission; else throw new IllegalArgumentException("Commission " + "must be between 0.0 and 1.0"); } @Override public double salary() { return getCommission() * getSales(); } @Override public String toString() { return String.format("%snSales: %, .2fnCommission: %,.2f", super.toString(),getSales(),getCommission()); } }
Polymorphism by Interfaces
Polymorphism by interfaces is quite similar to the previous example, except that here the polymorphic rules are according to the norms specified by Java interface. An interface name can be used as a reference variable as we have done with the class name above. It refers to any object of any class that implements the interface. Here is an example.
package org.mano.example; public interface Player { public enum STATUS{PLAY,PAUSE,STOP}; public void play(); public void stop(); public void pause(); } package org.mano.example; public class VideoPlayer implements Player { private STATUS currentStatus = STATUS.STOP; @Override public void play() { if(currentStatus == STATUS.STOP || currentStatus == STATUS.PAUSE) { currentStatus = STATUS.PLAY; System.out.println("Playing Video..."); } else System.out.println("I am ON playing man!"); } @Override public voidstop() { if(currentStatus == STATUS.PLAY || currentStatus == STATUS.PAUSE) { currentStatus = STATUS.STOP; System.out.println("Video play stopped."); } else System.out.println("Do you want me to go fishing?"); } @Override public void pause() { if(currentStatus == STATUS.PLAY) { currentStatus = STATUS.PAUSE; System.out.println("Video play paused."); } else System.out.println("I'm a statue. You froze me already!"); } } package org.mano.example; public class AudioPlayer implements Player { private STATUS currentStatus = STATUS.STOP; @Override public void play() { if(currentStatus == STATUS.STOP || currentStatus == STATUS.PAUSE) { currentStatus = STATUS.PLAY; System.out.println("Playing Audio..."); } else System.out.println("I am ON playing man!"); } @Override public void stop() { if(currentStatus == STATUS.PLAY || currentStatus == STATUS.PAUSE) { currentStatus = STATUS.STOP; System.out.println("Audio play stopped."); } else System.out.println("Do you want me to go fishing?"); } @Override public void pause() { if(currentStatus == STATUS.PLAY) { currentStatus = STATUS.PAUSE; System.out.println("Audio play paused."); } else System.out.println("I'm a statue. You froze me already!"); } } package org.mano.example; public class PlayerApp { public static void main(String[] args) { Player player= new VideoPlayer(); player.play(); player.pause(); player.stop(); player= new AudioPlayer(); player.play(); player.pause(); player.stop(); } }
Note that in PlayerApp we have used the interface Player to declare an object reference variable. The reference variable player can refer to any object of any class which implements the Player interface. To demonstrate that, here we have used the same player variable to refer to both VideoPlayer object and AudioPlayer object. The methods invoked at runtime are those specific to the class object it refers to. The relationship between the class which implements the interface and the interface itself is of parent and child, as we have seen in the example of polymorphism with inheritance. It is also a is-a relationship and forms the basis of the polymorphism.
Conclusion
The difference between implementing polymorphism by class inheritance or via interface is a matter of choice. In fact, the distinction lies in understanding the properties and characteristics of class and interface. There is no strict rule to define what to use when, except through understanding their nature. This is out of the scope of this article. But, in polymorphism, both the idea suits and is quite capable in doing what we want to do with them. That’s all.