Java OOPs Simplified - Part 4

Java OOPs Simplified - Part 4

This article gives an insight into Polymorphism and different ways to achieve it- Run-time Polymorphism & Compile-time Polymorphism

POLYMORPHISM

Polymorphism is one of the 4 major pillars of OOPs. The other three are Inheritance, Abstraction and Encapsulation.

"Polymorphism" is derived from 2 Greek words, where 'poly' means "many" and 'morphs' means "forms", so in turn polymorphism means "many forms".

So in Java, it's the way of performing a task in different ways, i.e., the same object or method can perform different things in different scenarios.

Now, let me take an example to explain it in a much easier way. The most simple and commonly stated example is that of Shapes. Imagine you have a drawing program that can draw different shapes like circles, squares, and triangles. Each shape can be drawn on the screen, but they have their unique ways of drawing.

So let's see how this works in code.

// this is a common Shape interface
interface Shape {
    void draw();
}

// Let's create specific shape classes that implement Shape
class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Triangle");
    }
}

class Oval implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Oval");
    }
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Rectangle");
    }
}

public class Main {
    public static void main(String[] args) {
        // Im just creating an array of different shapes
        Shape[] shapes = new Shape[3];
        shapes[0] = new Triangle();
        shapes[1] = new Oval();
        shapes[2] = new Rectangle();

      // here we are using polymorphism to draw different shapes
        for (int i = 0; i < shapes.length; i++) {
         Shape shape = shapes[i];
         shape.draw();
       }
    }
}

So if I explain the above example;

  1. So here we have a common interface Shape with a method draw().

  2. We create specific shape classes (Triangle, Oval, Rectangle) that implements the Shape interface and provide their unique draw() implementations.

  3. In the Main class, we create an array of different shapes, each instantiated as a different type of shape (polymorphism).

  4. We use a for loop to call the draw() method on each shape, and even though we're using the same method name, each shape draws itself the respective shape based on its specific implementation.

So now you have got an idea about the purpose of Polymorphism. Now let's look at the different ways to achieve this.

  1. Method Overloading

  2. Method Overriding

Method Overloading

In method overloading, we can use the method with the same name but different parameters within a single class. So in one class, you can have many methods with the same name but different types or different numbers of parameters.

It is also called compile time polymorphism because in the compile time java determines which method to be called based on the number or type of arguments used for method calling.

NB: Parameters of a method is also called method signature.

NB: Return type is not considered while we overload a method, which means 2 methods within a class can have the same or different return types.

NB: Minimum number of classes required to overload a method is 1.

Let's now look at a simple example of method overloading in Java.

public class Calculator {
    // a method to add two numbers
    public int add(int a, int b) {
        return a + b;
    }

    // a method to add three numbers
    public int add(int a, int b, int c) {
        return a + b + c;
    }

    // a method to add two double type numbers
    public double add(double a, double b) {
        return a + b;
    }

    public static void main(String[] args) {
        Calculator c1 = new Calculator();

        int result1 = c1.add(7, 11);
        int result2 = c2.add(4, 3, 9);
        double result3 = c3..add(6.5, 1.9);

        System.out.println("method1= " + result1); // output: method1= 18
        System.out.println("method2= " + result2); // output: method1= 16
        System.out.println("method3= " + result3); // output: method1= 8.4
    }
}

In the above example, the class Calculator used the method with the name add more than once within the class with different numbers and different types of parameters.

If you look at the method you can see an object with name c1 is created and it is used to invoke the methods. Also, it's very important to note that, you have to send the arguments matching the parameters when we call those methods. If you provide arguments of incorrect type or number it will result in a "compilation error".

But if you look at the return type we have used the same as well as different return types. So it's clear that return type is not important in method overloading.

Method Overriding

In method overriding we can use the method with the same name and the same parameters in different classes. So it allows the child class to provide a specific implementation of a method that is already defined in the parent class.

It is important to note that when the child class overrides the method, it provides a new implementation for that method with the same name, parameter and return type.

It is also an example of rum time polymorphism.

NB: For overriding, we require at least 2 classes.

NB: The child class cannot use more restrictive access modifiers than the parent class, which means if the parent class method is public, then child class method should be either public or protected. It cannot be "private".

NB: The overriding method should have the same method name, parameters and return types.

Now let's have a look at an example of overriding.

class ParentCalculator {
    int add(int a, int b) {
        return a + b;
    }
}

class ChildCalculator extends ParentCalculator {
    // Method overriding for add
    @Override
    int add(int a, int b) {
        return a + b + 10; // Adds 10 more than the base class, 
                           // because we can make changes only in this portion.
    }
}

public class Main {
    public static void main(String[] args) {
        ParentCalculator c1 = new ParentCalculator();
        ChildCalculator c2 = new ChildCalculator();

        int result1 = c1.add(7, 11);         // Calls the base class method
        int result2 = c2.add(4, 16);         // Calls the overridden method

        System.out.println("Result1: " + result1); // Result1: 18
        System.out.println("Result2: " + result2); // Result2: 30
    }
}

In this example, we have a ParentCalculator class with an add a method that adds two numbers. The ChildCalculator class extends ParentCalculator and overrides the add a method to add '2' to the result.

When we create instances of ParentCalculator and ChildCalculator and call the add method, you can see how method overriding works.

One of the important questions is that is it possible to override a static method. And the answer is "NO", it's not possible to override a static method as it is bound by a class. Like that we cannot override a main method. Because it's a static method.

So this is how method overloading and method overriding work in Java.