Polymorphism in Java [Detailed Explanation]

Polymorphism in Java [Detailed Explanation]

In this article, we'll learn and discuss about the Polymorphism property along with their detailed approach using Java. Let's start!

In Java, polymorphism refers to the ability of a single object or method to have multiple forms. This can be achieved through a process called overriding, where a subclass provides a different implementation of a method that is already defined in its superclass. Polymorphism is a key feature of object-oriented programming and allows for more dynamic and flexible code. It is also a way of achieving runtime polymorphism.

Method Overloading

Method overloading in Java allows a class to have multiple methods with the same name, but with different parameters. This allows for more flexibility and ease of use when calling the methods, as the appropriate method will be called based on the number and type of arguments passed to it.

Here is an example of method overloading in Java:

class MyClass {
    public void print(int x) {
        System.out.println(x);
    }

    public void print(String s) {
        System.out.println(s);
    }

    public void print(double d) {
        System.out.println(d);
    }
}

In this example, the class MyClass has three methods named print, each with a different parameter. When calling the print method, the correct version of the method will be called based on the type of the argument passed to it.

MyClass obj = new MyClass();
obj.print(5); // calls the first print method
obj.print("Hello World"); // calls the second print method
obj.print(3.14); // calls the third print method

It's important to note that when overloading methods in Java, the parameter lists must differ in either number of parameters or type of parameters.

Method Overriding

Method overriding in Java allows a subclass or child class to provide a specific implementation for a method that is already defined in its superclass or parent class. This allows for polymorphism, where an object can be treated as an instance of its superclass, but still use the specific implementation of a method in its subclass.

Here is an example of method overriding in Java:

class Shape {
    public void draw() {
        System.out.println("Drawing a shape");
    }
}

class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

In this example, the class Shape has a method named draw, which simply prints "Drawing a shape". The class Circle extends Shape and overrides the draw method with its own implementation, which prints "Drawing a circle".

Shape s = new Circle();
s.draw(); // prints "Drawing a circle"

When a subclass overrides a method, it must have the same method signature (i.e. the same method name, return type and parameter list) as the method in the superclass. Additionally, the access level must be at least as accessible as the super class method.

It's also important to note that the @Override annotation is optional, but it's good practice to use it, because it helps to avoid errors. If you accidentally misspell the method name or change the parameter list, the code will not compile and will give you an error message.

Run-time Polymorphism

Runtime polymorphism, also known as dynamic method dispatch, is a feature of object-oriented programming languages in which a single method can have different implementations in different classes. The specific implementation of the method that is executed is determined at runtime based on the actual type of the object that the method is called on. This allows for a more flexible and extensible program design, as new classes can be added that inherit from existing classes and override their methods, without requiring changes to the code that calls those methods.

In Java, runtime polymorphism is achieved through method overriding. When a subclass overrides a method of its superclass, it is said to have runtime polymorphism. For example:

class Animal {
    public void speak() {
        System.out.println("Animal can speak");
    }
}

class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("Dogs bark");
    }
}

class Cat extends Animal {
    @Override
    public void speak() {
        System.out.println("Cats meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.speak();  // Prints "Dogs bark"

        animal = new Cat();
        animal.speak();  // Prints "Cats meow"
    }
}

In the above example, the speak() method of the Dog and Cat classes are overriding the speak() method of the Animal class. The type of the object being referred to by the animal variable determines which version of the speak() method is executed at runtime. This is known as runtime polymorphism.

Compile-time Polymorphism

Compile-time polymorphism, also known as static polymorphism, is achieved through method overloading in Java. Method overloading allows you to have multiple methods with the same name, but with different parameter lists. The appropriate method to be called is determined at compile-time, based on the number and type of arguments passed.

Here is an example of method overloading in Java:

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

    public double add(double a, double b) {
        return a + b;
    }

    public int add(int a, int b, int c) {
        return a + b + c;
    }
}

In this example, the class Calculator has three methods named add, each with a different parameter list. When the add method is called, the compiler will determine which version of the method to call based on the number and type of arguments passed.

For example,

Calculator calculator = new Calculator();
int result1 = calculator.add(1, 2);
double result2 = calculator.add(1.5, 2.5);
int result3 = calculator.add(1, 2, 3);

In this example, the first call to add will call the first version of the method, the second call will call the second version, and the third call will call the third version.

super Keyword

In Java, the super keyword is used to refer to the immediate parent class of an object or class. It can be used to call the parent class's methods or constructors, or to access the parent class's variables.

Here is an example of how the super keyword can be used to call the parent class's constructor:

class Parent {
    public Parent() {
        System.out.println("Parent class constructor");
    }
}

class Child extends Parent {
    public Child() {
        super();
        System.out.println("Child class constructor");
    }
}

In this example, the Child class extends the Parent class. The Child class's constructor calls the super() method, which in turn calls the parent class's constructor. If we create an object of Child class

Copy codeChild obj = new Child();

This will output:

Parent class constructor
Child class constructor

The super keyword can also be used to access variables in the parent class. For example, if the parent class has a variable named x, the child class can access it using the super.x syntax.

super keyword also can be used to call the parent class's method if the child class has overridden the parent class's method with the same signature. For example,

class Parent {
    public void print() {
        System.out.println("Parent class");
    }
}

class Child extends Parent {
    public void print() {
        super.print();
        System.out.println("Child class");
    }
}

Here, when we create an object of Child class and call the print() method

Child obj = new Child();
obj.print();

This will output:

Parent class
Child class

It calls the parent class's print() method first and then the child class's print() method.

final Keyword

In Java, the "final" keyword is used to indicate that a variable, method, or class cannot be modified.

When applied to a variable, it means the value of the variable cannot be changed once it has been assigned. For example:

final int x = 5;
x = 10; // This would cause a compile error

When applied to a method, it means the method cannot be overridden by a subclass. For example:

class A {
  final void myMethod() {
    // method code
  }
}
class B extends A {
  void myMethod() { // This would cause a compile error
    // new method code
  }
}

When applied to a class, it means the class cannot be subclassed. For example:

final class A {
  // class code
}
class B extends A { // This would cause a compile error
  // new class code
}

Additionally, final can also be used as a non-access modifier in a class member variable. When it is used as a non-access modifier, it means that the variable must be initialized at the time of declaration and the value of the variable cannot be changed again.

instanceof Operator

In Java, the "instanceof" operator is used to determine if an object is an instance of a particular class or interface. The operator returns a boolean value indicating whether the object can be cast to the specified class or interface without throwing a ClassCastException. The syntax of the operator is as follows:

object instanceof class

where "object" is the instance that you want to check, and "class" is the class or interface to check against. For example:

String str = "Hello";
boolean result = str instanceof String; // result is true

It can also be used to check if an object is an instance of a particular interface or its implementation class

List<Integer> list = new ArrayList<>();
result = list instanceof List; // result is true
result = list instanceof ArrayList; // result is true

It is important to note that the instanceof operator will return false if the object is null.

Also, the instanceof operator is only applicable to reference type. It will not work with primitive types like int, long, double etc.

Dynamic Binding

Dynamic binding in Java is the process of linking a method call to the actual method implementation at runtime, rather than at compile-time. This is made possible through the use of polymorphism and the ability for an object to take on multiple forms.

Here is an example of dynamic binding in Java:

class Animal {
    public void speak() {
        System.out.println("Animal make different sounds.");
    }
}

class Dog extends Animal {
    public void speak() {
        System.out.println("Woof woof!");
    }
}

class Cat extends Animal {
    public void speak() {
        System.out.println("Meow meow!");
    }
}

public class DynamicBindingExample {
    public static void main(String[] args) {
        Animal a = new Dog();
        Animal b = new Cat();

        a.speak(); // Output: "Woof woof!"
        b.speak(); // Output: "Meow meow!"
    }
}

In this example, we have a class "Animal" that has a method "speak()" which will be overridden by the "Dog" and "Cat" classes. We have created two objects, one of Dog class and one of Cat class, and we are calling the speak method on both objects, the output will be different for both the object, the output will be the overridden method of the class, this is because of dynamic binding.

The speak() method is called on the objects "a" and "b", which are both of type "Animal". However, since "a" is an instance of "Dog" and "b" is an instance of "Cat", the correct version of the speak() method is called at runtime based on the actual type of the object, not the type of the reference. This is an example of dynamic binding in Java.

Advantages of Polymorphism

Polymorphism is a powerful feature of object-oriented programming languages like Java that allows objects of different classes to be treated as objects of a common superclass. There are several advantages of polymorphism in Java:

  1. Code Reusability: Polymorphism allows objects of different classes to be used interchangeably, which leads to more reusable code.

  2. Flexibility: Polymorphism allows you to write code that can handle objects of different classes in a single way, making your code more flexible and adaptable to changes.

  3. Extensibility: Polymorphism allows new classes to be added to a program without modifying existing code, which makes the program more extensible.

  4. Abstraction: Polymorphism allows you to work with objects at a higher level of abstraction, rather than having to deal with the details of their specific classes.

  5. Simplicity: Polymorphism allows you to simplify your code by eliminating the need for explicit type checking and casting.

  6. Dynamic Binding: Polymorphism allows the correct method to be called at runtime, based on the actual type of the object, rather than the type of the reference.

  7. Dynamic method dispatch: In polymorphism, the method that is to be executed is decided at runtime based on the actual object that is being referred, and not at the time of compilation.

  8. Late binding: Polymorphism allows the binding of methods and variables to occur at runtime which is called late binding or dynamic binding.


More articles on similar concepts/topics,

  1. Introduction to Object-Oriented Programming.

  2. Inheritance in Java [Detailed Explanation].

  3. Introduction to Python programming language.

  4. Array vs Linked List - Which one is better?

  5. Simplicity and Beauty of Recursion.

Did you find this article valuable?

Support Abhishek Sharma by becoming a sponsor. Any amount is appreciated!