Architecture & DesignWhat is Polymorphism in Java?

What is Polymorphism in Java?

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Polymorphism in Java is closely associated with the principle of inheritance. The term “polymorphic” means “having multiple forms.” Polymorphism in Java simplifies programming by providing a single interface overlaid with multiple meanings as it goes through the rigor of subclassing. This article is a attempt to explore the concept with a focus on Java with appropriate illustrations and examples.

Polymorphism in Java Leverages Extensibility

Polymorphism leverages extensibility. That means we can assign new classes with almost no modification of the existing code, provided the class is part of the inheritance hierarchy. The new class becomes part of the classification, like a Lego attached to a construction in such a manner that the construction would not crumble even if we detach one. As per the norms of inheritance, a new class acquires the property and methods of the superclass and is open to override only those methods that it is interested in modifying. Other derived implementation may be used as it is implemented by the superclass without making any changes.

For example, a Shape class contains a method called area(). Now, if we extend this class to create a Rectangle class (the area formula is quite different from a triangle; had we created a class called Triangle), we need to write only the Rectangle class and part of the code that instantiates the Rectangle object. However, the Rectangle has the option to use the generic implementation of the area() method defined by its superclass; otherwise, we can modify it to meet our specific needs. So, polymorphism in Java is in a way to enable us to create a class in general, which becomes more specific as we extend it through sub-classification.

Dynamic Binding

Typically, the type of the reference variable exactly matches with the class object that it refers to. This, however, is not always the case with polymorphism. For example,

Poly1
Figure 1: A shape hierarchy chart

Triangle triangle;

The triangle is a reference to the Triangle class object; it can be equally used to refer to the object of IsocelesTriangle. The variable type and the object it refers to must be compatible, but their types need not be the same. This is called polymorphic reference. A polymorphic reference can refer to different objects at different points of time. We always can create a reference to the topmost class object in the inheritance hierarchy, such as,

Shape s1;
s1=new Triangle();         // OK
IsocelesTriangle isoTri;
isoTri=new Triangle();     // Not OK...

A superclass reference variable can refer to a subclass object, but a subclass reference variable cannot refer to a superclass object (unless explicitly casted, which we shall later down the line).

Suppose there is an abstract method called area() declared in the abstract Shape class. This means that every subclass must provide the implementation of that method. So, the call to the area() method must be different under different circumstances.

Shape s1;
s1=new Triangle();
// area() method defined in the Triangle class
s1.area();
s1=new Rectangle();
// area() method defined in the Rectangle class
s1.area();

The specific area() method invoked through the reference variable depends on the reference to the object it holds at a certain point in time. In the preceding example, when the variable s1 holds the reference to the Triangle class object, an invocation to area() will call the implementation defined in the Triangle class. And, when s1 refers to the Rectangle class object, the invocation to area() methods invokes the area() method defined in the Rectangle class.

This commitment is referred to as binding a method invocation to a method definition. Although in other situations, this binding of a method invocation to a method definition can occur at compile time, in polymorphic reference this decision of binding is not possible until runtime. The method definition to use is based on the object referred to by the reference variable at that particular moment. The following example will clarify the idea.

Shape s1;
double result;
if(choice==0)
   s1=new Triangle();
else
   s1=new Rectangle();
result=s1.area();

Observe that the binding of invocation to the area() method to the implementation defined in Rectangle or Triangle can only be decided upon the value of the choice variable checked conditionally by the if statement. The invocation to area() being bounded to the definition of Rectangle or Triangle is equally probable. The decision can only be ascertained at runtime.

This is referred to as late binding or dynamic binding. Dynamic binding is obviously less efficient than static binding or binding at compile time, yet the overhead is more acceptable in view of flexibility.

Polymorphism in Java

There are two ways to create polymorphic references in Java: using inheritance via class and via interface.

Polymorphism via Class

Referencing variables by using a class name is used to refer to any object of that class including subclasses. For example, if a Parallelogram class is a superclass of Rectangle, then we can write as follows:

Parallelogram pl;
Rectangle res=new Rectangle();
pl=rec;

The reverse operation such as assigning a Parallelogram object reference to the Rectangle reference variable can also be done with explicit cast but it would make less sense because a rectangle is always a parallelogram but a parallelogram may not be a rectangle. As a result though it may be syntactically correct but make little sense semantically. So it is a better to go other way round than forcefully cast a object reference variable to refer to one of its super classes.

Consider a quick example of a polymorphic class hierarchy and its implementation in Java.

Poly2
Figure 2: A polymorphic class hierarchy in Java

Observe that Employee is implemented as an abstract class (see Listing 1). This ensures that any direct object of this class cannot be created. The class declares an abstract method called pay(), without any definition. This method must be implemented by the extended subclasses or the subclass must be an abstract class, or an another interface.

The extended subclasses HourlyEmployee (see Listing 2) and SalariedEmployee (see Listing 3) provide a customized definition of the inherited pay() method.

Also, observe that, in the Company class (see Listing 4), we have created an array of Employee reference object variables where references to the instances of its subclasses are stored. When the polymorphic pay() method is invoked, depending upon the reference to object instance at the moment, the appropriate pay() method executes. This decision of associating a method call to method definition is done dynamically at runtime.

package org.mano.example;

public abstract class Employee {

   private int empCode;
   private String firstName;
   private String lastName;
   public Employee(int empCode, String firstName,
         String lastName) {
      super();
      this.empCode = empCode;
      this.firstName = firstName;
      this.lastName = lastName;
   }

   public int getEmpCode() {
      return empCode;
   }

   public void setEmpCode(int empCode) {
      this.empCode = empCode;
   }

   public String getFirstName() {
      return firstName;
   }

   public void setFirstName(String firstName) {
      this.firstName = firstName;
   }

   public String getLastName() {
      return lastName;
   }

   public void setLastName(String lastName) {
      this.lastName = lastName;
   }

   public abstract double pay();

   public String toString() {
      return String.format("%s %s (code: %d)n",
         getFirstName>(), getLastName(), getEmpCode());
   }
}

Listing 1: Employee.java

package org.mano.example;

public class HourlyEmployee extends Employee {

   private double rate;
   private double workingHours;

   public HourlyEmployee(int empCode, String firstName,
         String lastName, double rate, double workingHours) {
      super(empCode, firstName, lastName);
      setRate(rate);
      setWorkingHours(workingHours);
   }

   public double getRate() {
      return rate;
   }

   public void setRate(double rate) {
      this.rate = (rate < 0.0) ? 0.0 : rate;
   }

   public double getWorkingHours() {
      return workingHours;
   }

   public void setWorkingHours(double workingHours) {
      this.workingHours = (workingHours < 0.0) ? 0.0 :
         workingHours;
   }

   @Override
   public double pay() {
      return getRate() * getWorkingHours();
   }
}

Listing 2: HourlyEmployee.java

package org.mano.example;

public class SalariedEmployee extends Employee {

   private double basic;
   private double grossSale;
   private double commissionRate;

   public SalariedEmployee(int empCode,String firstName,
      String lastName, double basic,
      double grossSale, double commissionRate) {
         super(empCode,firstName, lastName);
         setBasic(basic);
         setGrossSale(grossSale);
         setCommissionRate(commissionRate);
   }

   public double getBasic() {
      return basic;
   }

   public void setBasic(double basic) {
      this.basic = (basic < 1000) ? 1000 : basic;
   }

   public double getGrossSale() {
      return grossSale;
   }

   public void setGrossSale(double grossSale) {
      this.grossSale = (grossSale < 0.0) ? 0.0 : grossSale;
   }

   public double getCommissionRate() {
      return commissionRate;
   }

   public void setCommissionRate(double commissionRate) {
      this.commissionRate = (commissionRate > 0.0 &&
      commissionRate < 1.0) ? commissionRate : 0.0;
   }

   @Override
   public double pay() {
      return getBasic() + getCommissionRate() * getGrossSale();
   }
   }

Listing 3: SalariedEmployee.java

package org.mano.example;

public class Company {

   private Employee[] employees;

   public Company() {
      employees = new Employee[5];
      employees[0] = new HourlyEmployee(6021, "Joshua", "Bloch",
                     1000.00, 65.67);
      employees[1] = new HourlyEmployee(6022, "Arun", "Verma",
                     7800.00, 23.95);
      employees[2] = new SalariedEmployee(6023, "David", "Blair",
                     5000.00, 256, 0.45);
      employees[3] = new HourlyEmployee(6024, "Sandip", "Patil",
                     7800.00, 23.95);
      employees[4] = new SalariedEmployee(6025, "Mike", "Paul",
                     2000.00, 567, 0.66);
   }

   public void payroll() {
      for (int i = 0; i < employees.length; i++) {
         System.out.print(employees[i].toString());
         System.out.printf("Pay Amount: %.2fn", employees[i].pay());
         System.out.println("----------------------------------");
      }
   }

   public static void main(String[] args) {
      Company c = new Company();
      c.payroll();
   }
}

Listing 4: Company.java

Polymorphism via Interface

Sometimes, it makes sense to implement a superclass as an interface rather than an abstract class. Abstract classes more appropriately suit the class design where there is a provision of defining the implementation of one or more of its member methods. It may also be the case that all the member methods are defined and none of them are abstract; maybe we just want to restrict creating a direct object of the class and force it to be extended by inheritance. As we jestingly say, it is a “half-breed” abstract. It can go either way, become a pure abstract class or be an abstract class only for the namesake.

In the same line, we can say that an interface is a “pure-breed” abstract class where no member method definitions are allowed (except default methods, since Java 8). Thus, if our design decision is to create a pure abstract class without defining any of its member functions, it is the interface that we should use.

Similar to creating a polymorphic reference using classes, we also can create a polymorphic reference using an interface. Here, we also use an interface name as the type of a reference variable. This reference variable can be used to refer to any object of any class that implements that interface.

package org.mano.example;

public interface IntFaceA {
   public void func();
}


package org.mano.example;

public class ClassA implements IntFaceA{
   @Override
   public void func() {
      System.out.println("func in class A");
   }
}


package org.mano.example;

public class ClassB implements IntFaceA{

   @Override
   public void func() {
      System.out.println("func in class B");
   }
}

That means we can write:

IntFaceA ia=new ClassB();
ia.func();

ia=new ClassA();
ia.func();

But, there is a catch there. Suppose ClassA and ClassB also contain some member function, as follows:

public class ClassA implements IntFaceA{

   public void memberOfClassA(){
         System.out.println("member function of class A");
   }

   @Override
   public void func() {
      System.out.println("func in class A");
   }
}


package org.mano.example;

public class ClassB implements IntFaceA{

   public void memberOfClassB(){
      System.out.println("member function of class B");
   }

   @Override
   public void func() {
      System.out.println("func in class B");
   }
}

Now, if we want to use an interface reference variable to invoke a function defined in any of its implemented subclasses, which is not declared in their super interface, the compiler produces an error. That means:

IntFaceA ia=new ClassB();
ia.func();
ia.memberOfClassB();   // error! this is not allowed

ia=new ClassA();
ia.func();
ia.memberOfClassA();   // error! this is not allowed

The reason is that the compiler can determine only that the object is of type IntFaceA. As a result, it only can be referred to respond to the method calls declared in the interface itself, such as func(). At a later point, the reference variable ia refers to the object of ClassA, which has a different member function; it does not allow the invocation.

However, we can force the invocation by explicit cast as follows:

IntFaceA ia=new ClassB();
((ClassB) ia).memberOfClassB();
ia=new ClassA();
((ClassA) ia).memberOfClassA();

This is possible only in a situation where we know that such an invocation is valid.

Also, we can use a polymorphic reference as a formal parameter to a method. This is a powerful technique to control the type of parameters passed to a method with the flexibility of accepting arguments of various types. For example:

void flexiMethod(IntFaceA param){
   // ...
}

Conclusion

Polymorphism in Java is powerful technique and, when used efficiently, makes the class design flexible and extensible with an easy-to-create customized implementation. This is such a basic property of Java that one cannot even put one foot forward without understanding what polymorphism is or how to implement it in Java. This article tried to give a glimpse of some intricacies which perhaps will be helpful for you to code better in Java.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories