Whether you are completely new to Java, or you are a senior software engineer that has used Java for decades, you will have to understand what polymorphism is and how to use it. On the surface it may seem straightforward, but there are many complex features like static and dynamic binding. Understanding what polymorphism is and how to use it will help you to write better object oriented programs.
What is Polymorphism
Polymorphism means "many forms". In Java, it means many forms of a method.
For example, lets say we have a class called GeometricObject
. It has 3 data fields for its color, date created, and whether it is filled or not. It has 2 constructors, accessors and mutators for the data fields, and a toString
method.
import java.util.Date;
public class GeometricObject {
private String color = "white";
private boolean filled = false;
private Date dateCreated = new Date();
public GeometricObject() {
}
public GeometricObject(String color, boolean filled) {
this.color = color;
this.filled = filled;
}
public GeometricObject(String color) {
this.color = color;
}
public GeometricObject(boolean filled) {
this.filled = filled;
}
public Date getDateCreated() {
return dateCreated;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public boolean isFilled() {
return filled;
}
public void setFilled(boolean filled) {
this.filled = filled;
}
@Override
public String toString() {
return "GeometricObject{" +
"color='" + color + '\'' +
", filled=" + filled +
", dateCreated=" + dateCreated +
'}';
}
}
If we have a new class called Circle
that is a subclass of GeometricObject, it will have the same data fields and methods as the superclass, but we will add one more data field called radius. We will also add 2 extra methods for getting the area and circumference of the circle.
public class Circle extends GeometricObject {
private double radius = 1;
public Circle() {}
public Circle(String color, boolean filled, double radius) {
super(color, filled);
this.radius = radius;
}
public Circle(String color, boolean filled) {
super(color, filled);
}
public Circle(double radius) {
this.radius = radius;
}
public double getArea() {
return Math.PI * radius * radius;
}
public double getCircumference() {
return 2 * Math.PI * radius;
}
@Override
public String toString() {
return "Circle{" +
"radius=" + radius +
'}';
}
}
The inheritance relationship between Circle
and GeometricObject
allows Circle
to inherit methods from GeometricObject
and add its own methods as well. Every instance of subclass is an instance of its superclass; therfore, an instance of a Circle
is also an instance of a GeometricObject
. Look at the following method called displayObject
.
public static void displayObject(GeometricObject object) {
System.out.println(object);
}
This method can not only be called with an instance of GeometricObject
but also with an instance of Circle
, because every instance of a Circle
is also an instance of a GeometricObject
. The following method call would be valid code:
displayObject(new Circle(3));
It would display:
Circle{radius=6.0}
The main thing to take away from this is that a supertype may also refer to a subclass of the supertype.
Dynamic Binding
Before we get into what dynamic binding is, it is important to distinguish between what the declared type and the acutal type of an object are.
Declared and Actual Type
A variable's declared type is the type used when the variable is declared.
Object myCircle = new Circle();
In the declaration above, the type Object
is the declared type because it was the type used to declare the variable myCircle
.
The actual type of a variable is the actual class that is referenced by the object. If we use the same declaration, Circle
would be the actual type because myCircle
is an instance of Circle
.
The Idea Behind Dynamic Binding
To understand dynamic binding, you need to see it in action. Lets say we have a circle of radius 5, and we want to know if it is shaded or not.
Circle circle = new Circle(5);
System.out.println(circle.isFilled());
The first thing the JVM will do when it sees the print statement is check to see if the Circle
has the implementation of the method isFilled
. If it does, it will call that method. If it doesn't, it will look for the method in Circle's superclass GeometricObject
and call that method.
In this case the method isFilled
is not implemented in Circle
, but it is implemented in GeometricObject
, so it will call that method.
false
Another Example
Lets use the common toString
method as another example of dynamic binding.
System.out.println(new Circle(2));
In this case, the JVM will look for the implementation of the toString
method in Circle
and will try to call that method. It is there and it is called.
Circle{radius:2.0}
If the implementation wasn't there, it would go to the next level (i.e. a higher superclass in the hierarchy) and look for the implementation of the toString
method there; in this case, it would be GeometricObject
. If the toString
method wasn't there (because right now it is), it would go to the next level superclass, which is Object
.
Method Matching and Method Binding
The declared type of a variable will decide which method to run at compile time. The compiler will find a method that matches the method signature at compile time; keep in mind that there may be several methods with the same signature but have different implementations in the inheritance hierarchy.
The JVM binds the implementation of the method to the actual type of the object at runtime.
Wrapping Up
Polymorphism and dynamic binding can be challenging concepts to grasp at first, but once you start working with them and become familiar with them, your code will become much more maintainable.