An Introduction to Polymorphism

Share on:

Overview

Polymorphism (from Greek) means one shape many forms. In programming, it means to allow single symbol to perform different types of actions. It can also mean to provide single interface for entities of different types.

A basic example

Consider the following two lines of code:

12 + 3;
2"hello" + "world";

First line will add the two operands and produce the sum as the result. The second line will concatenate the two operands and produce the concatenated string as the result.

Same operator + is used but has different meaning and operation which depends on the type of operands used. This is the essence of polymorphism which means to allow single symbol to perform different actions based on the entity type in question.

Different types of Polymorphism

Ad-hoc Polymorphism / Function Overloading / Static Polymorphism

To put it in very simple terms, function overloading means two or more functions having same name but different argument list. It falls under the category of Ad-hoc polymorphism.

Consider the following scenario: I want to create a function which accepts two integers and returns an integer which is the sum of the arguments. Say I've written the following function:

1int addIntegers(int a, int b) {
2
3    return a + b;
4}
Now say I want to add two float values this time. Can I use the above created function? No, because it will only accept integer values. So I'll go ahead and create a new function say like this:
1int addIntegers(int a, int b) {
2
3    return a + b;
4}
5float addFloats(float a, float b) {
6
7    return a + b;
8}
Cool. Now say I want to add an int and a double value. The above two functions will not work for me so I'll go ahead and add a new function addIntegerDouble(). What about if I want to add an double and an int this time (changed the order). I will probably need another function with the name addDoubleInteger().

Can you see how a simple problem has become so complicated. Now I have in my system all these functions with different names which I have to remember (or refer back to documentation again and again) which are essentially doing the same thing: adding two numbers. They just differ in the type of arguments to be passed.

There's a better way to do this. Instead of giving each function a different name, which is essentially doing the same thing, we can simply give all these functions the same name: add()
 1int add(int a, int b) {
 2
 3    return a + b;
 4}
 5float add(float a, float b) {
 6
 7    return a + b;
 8}
 9double add(double a, int b) {
10
11    return a + b;
12}
13double add(int a, double b) {
14
15    return a + b;
16}
This is the essence of function overloading! Java allows me to declare functions with same names and only differ in argument list and/or return type. Function overloading will reduce so much complexity because I won't have to remember all the different function names which are essentially doing the same thing. Now if I write something like:
1int a = 4, b = 9;
2int c = add(a, b);
Compiler will know which variant of add() to call based on the type of arguments passed. As this decision is made by compiler at compile time, this kind of polymorphism is also referred to as Static Polymorphism or Compile-time polymorphism

Subtyping Polymorphism / Runtime Polymorphism / Dynamic Polymorphism

Let's consider the following design: Shape design We have different kinds of entities in our system: Square, Triangle, Rectangle, Circle and Rhombus. We understand which class extends which one and which class implement or override which methods by looking at the above diagram, right?

Now let's say we need functions which will take in different kind of shape and call it's rotate() method. Something like this:

1public void callRotate(Square s) {
2
3    s.rotate();
4}
Function overloading tells us that we can keep the name of the function same and just change the argument types. So we can create this callRotate() function for each of our entity in the following manner:
 1public void callRotate(Square s) {
 2
 3    s.rotate();
 4}
 5public void callRotate(Circle c) {
 6
 7    c.rotate();
 8}
 9public void callRotate(Triangle t) {
10
11    t.rotate();
12}
13public void callRotate(Rectangle r) {
14
15    r.rotate();
16}
17public void callRotate(Rhombus r) {
18
19    r.rotate();
20}
But still is it the best way to do things? What will happen if in my design I add a new subclass tomorrow? I will have to make sure I add the new variant of callRotate() method here. What will happen if my code also has different variants of callPlaySound()? If I add a new subclass I will have to make sure I make relevant entries at all places. This practice is not scalable and you can clearly see where the problem is.
Here comes Subtyping Polymorphism. Let's see an example:
1Square square = new Square();
Till now we've seen that the type of reference and the type of object should match, something like in above example. But will polymorphism we can also do something like the following:
1Square square = new Square();
2Shape square = new Square();    /*Wow!*/
With polymorphism, the reference type can be a superclass of the actual object type! How does it helps us? Well now let's come back to our callRotate() method. With this new power unlocked we can simply do something like:
1public void callRotate(Shape s) {
2
3    s.rotate();
4}
We can simply declare just this one version of callRotate() function and remove else. As this function takes in a reference variable of Shape type, we will be able to pass any kind of shape, be it Square, Circle, Triangle, Rectangle and Rhombus to it and this function will take care of calling the appropriate rotate() method of appropriate type. Mind = Blown!
Now this is writing scalable code. Now I can add any number of Shape's subclasses without worrying to write a separate callRotate() method for this new subtype. This one function will take care of all.

Let's take another example. Say I want to collect 2 squares, 2 rectangles, 3 circles, 3 triangles and 2 rhombuses in arrays and call rotate() methods on each of these objects. I'll probably go and solve this problem as follows:
 1Square[] squareArray = new Square[2];
 2squareArray[0] = new Square();
 3squareArray[1] = new Square();
 4squareArray[0].rotate();
 5squareArray[1].rotate();
 6
 7Rectangle[] rectangleArray = new Rectangle[2];
 8rectangleArray[0] = new Rectangle();
 9rectangleArray[1] = new Rectangle();
10rectangleArray[0].rotate();
11rectangleArray[1].rotate();
12
13Circle[] circleArray = new Circle[3];
14circleArray[0] = new Circle();
15circleArray[1] = new Circle();
16circleArray[2] = new Circle();
17circleArray[0].rotate();
18circleArray[1].rotate();
19circleArray[2].rotate();
20
21Triangle[] triangleArray = new Triangle[3];
22triangleArray[0] = new Triangle();
23triangleArray[1] = new Triangle();
24triangleArray[2] = new Triangle();
25triangleArray[0].rotate();
26triangleArray[1].rotate();
27triangleArray[2].rotate();
28
29Rhombus[] rhombusArray = new Rhombus[2];
30rhombusArray[0] = new Rhombus();
31rhombusArray[1] = new Rhombus();
32rhombusArray[0].rotate();
33rhombusArray[1].rotate();
Is it the best way to do things? Certainly doesn't feel so. Using subtyping polymorphism, we can create something called Polymorphic Arrays and reduce the complexity of this program drastically. Let's rewrite the above program:
 1Shape[] shapeArray = new Shape[12];
 2shapeArray[0] = new Square();
 3shapeArray[1] = new Square();
 4
 5shapeArray[2] = new Rectangle();
 6shapeArray[3] = new Rectangle();
 7
 8shapeArray[4] = new Circle();
 9shapeArray[5] = new Circle();
10shapeArray[6] = new Circle();
11
12shapeArray[7] = new Triangle();
13shapeArray[8] = new Triangle();
14shapeArray[9] = new Triangle();
15
16shapeArray[10] = new Rhombus();
17shapeArray[11] = new Rhombus();
18
19for (int i = 0; i < 12; i++) {
20
21    shapeArray[i].rotate();
22}

See how drastically I'm able to reduce the complexity of the code. I can collect all the objects in a single array and use a single loop to call rotate() on all objects. This is all possible just because of a single idea: With polymorphism, the reference type can be a superclass of the actual object type
As this decision as to which sub class's version of rotate() is called is made on runtime, this is also called Runtime polymorphism and Dynamic Polymorphism

Parametric Polymorphism

There's another type of polymorphism in Java called Parametric Polymorphism which will be explained in detail when we talk about generics.

Takeaways

  • Different use cases where polymorphism is helpful
  • Function overloading
  • Subtyping
  • Compile time / run time polymorphism
  • Polymorphic arrays
comments powered by Disqus