An Introduction to Polymorphism
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}
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}
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}
1int a = 4, b = 9;
2int c = add(a, b);
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:
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}
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}
Here comes Subtyping Polymorphism. Let's see an example:
1Square square = new Square();
1Square square = new Square();
2Shape square = new Square(); /*Wow!*/
callRotate()
method. With this new power unlocked we can simply do something like:
1public void callRotate(Shape s) {
2
3 s.rotate();
4}
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();
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