Getty Images
Understanding the role of polymorphism in OOP
Polymorphism is used in OOP to allow developers to write more efficient code and redefine methods for derived classes; however, it could raise real-time performance issues.
As one of the most widely used computer programming models, object-oriented programming organizes software design around objects instead of procedure or logic. All languages based on OOP must exhibit its four core characteristics: encapsulation, abstraction, inheritance and polymorphism.
While polymorphism raises various real-time performance issues, it has numerous benefits to offer while programming. It gives the ability to write more efficient code and redefine methods for derived classes, which makes polymorphism a must-learn concept in OOP.
Let's look at the polymorphism aspect of OOP, discover its broad types, advantages and pitfalls.
What is polymorphism?
Polymorphism is the ability for something to take on many forms. In terms of a programming language exhibiting this characteristic, we can create class objects that are inherited from the same parent class and have the same names but different behaviors. Variables and functions also exhibit polymorphic behavior in OOP by using a single interface with different underlying forms. This increases the language's ability to reuse the same lines of code on multiple occasions.
Apart from code reusability, polymorphism allows for a single variable name to be used to store variables of multiple data types such as int, float, long and double. Similarly, we can use simple abstractions to compose more complex and powerful ones that make debugging code easier.
Types of polymorphism
Polymorphism in OOP languages can largely be described in two categories: compile-time and runtime.
Compile-time polymorphism. To accomplish compile-time polymorphism, you use method overloading, in which functions share the same name but are different in terms of the number, types or order of arguments they accept. This means methods of the same name perform differently in given scenarios based on the data provided.
Using compile-time polymorphism helps developers perform only one operation in different scenarios so they don't need to define functions with different names. Moreover, having the same function names improves code readability.
For example, let's say we want to write a program to add a given set of numbers but there can be any number of arguments. Let's say the number is either 2 or 3 for this example. It doesn't make sense to define many functions that achieve the same task of addition and yet have different names. As a solution, we perform method overloading to write the program.
public class MethodOverloading {
public static void add(int a, int b)
{
int sum = a + b;
System.out.println(sum);
}
public static void add(int a, int b, int c)
{
int sum = a + b + c;
System.out.println(sum);
}
public static void add(double a, double b)
{
double sum = a + b;
System.out.println(sum);
}
public static void main(String[] args) {
add(10,10);
add(10,10,10);
add(10.2,-3.1);
}
}
Output:
20
30
7.1
Runtime polymorphism. Achieved through method overriding and virtual functions determined during runtime, dynamic polymorphism takes inheritance into account during its implementation. For method overriding to take place, a subclass must have the same method as defined in the parent class. Here, a call to a single overridden method is answered during runtime when -- say, in case of Java, -- a JVM detects an appropriate method for execution when a subclass is assigned to its parent form. This intervention is essential as the subclass could override all the methods defined in the parent class.
class ParentClass {
public void overrideMethod(){
System.out.println("This method is overridden.");
}
}
public class ChildClass extends ParentClass{
public void overrideMethod(){
System.out.println("This method will override.");
}
public static void main(String[] args) {
ParentClass obj = new ChildClass();
obj.overrideMethod();
}
}
Output:
This method will override.
Whenever an overridden method is called through a reference of parent class, the object type determines which method will be executed during runtime and is done by the JVM. For instance, the code could look like this:
ParentClass obj = new ParentClass();
obj.overrideMethod();
//overrideMethod() of ParentClass is called.
ChildClass obj = new ChildClass();
obj.overrideMethod();
//overrideMethod() of ChildClass is called.
ParentClass obj = new ChildClass();
obj.overrideMethod();
//overrideMethod() of ChildClass is called since the object belongs to //the child class.
Pitfalls of polymorphism
There are a few uncertainties in polymorphism that can lead to runtime errors if not properly handled.
Problem with type identification during downcasting. We lose access to some subtype-specific methods when we perform an upcast that can be solved through a downcast, but this method doesn't guarantee type checking. JVM performs a run-time type information check to solve this problem, but we can also attempt an explicit type identification using instanceof keyword.
Problem with a fragile base class. Base or superclasses are generally considered fragile, even if safe modifications are made to them. This could cause the derived class to malfunction.
public class ParentClass {
private String string;
void writeContent(String content) {
this.string = string;
}
void toString(String str) {
str.toString();
}
}
public class ChildClass extends ParentClass {
@Override
void writeString(String string) {
toString(string);
}
}
Suppose we modify the parent class to look like this:
public class ParentClass {
//...
void toString(String string) {
writeString(string);
}
}
The ChildClass goes into infinite recursion in the writeString() method, leading to stack overflow. We can use the final keyword to address this fragility by preventing subclasses from overriding the writeString() method.