Understanding Polymorphism in Java Programming

Sridhar KSridhar K
10 min read

Polymorphism

Poly means many, morphism means types or forms. Polymorphism in java is the ability to take on many forms. It allows you to use a child class object as a parent class object. This helps us to enable different behaviors at different instances of time.

Imagine a scenario where you have a base class called Animal and two derived classes, Cat and Dog. The Animal class has two methods: doEat() and doSound(). Each derived class overrides these methods to provide their own implementation. When an Animal object is created, you can call these methods to eat or make a sound, regardless of whether it's a Cat or Dog. This demonstrates polymorphism, where different objects of the same base class can exhibit different behaviors based on their specific implementations.

Program

// Base class
class Animal {
    void doEat() {
        System.out.println("Animal is eating");
    }

    void doSound() {
        System.out.println("Animal makes a sound");
    }
}

// Derived class Cat
class Cat extends Animal {
    @Override
    void doEat() {
        System.out.println("Cat is eating fish");
    }

    @Override
    void doSound() {
        System.out.println("Cat meows");
    }
}

// Derived class Dog
class Dog extends Animal {
    @Override
    void doEat() {
        System.out.println("Dog is eating chicken");
    }

    @Override
    void doSound() {
        System.out.println("Dog barks");
    }
}

public class TestPolymorphism {
    public static void main(String[] args) {
        // Creating Animal reference for Cat object
        Animal myAnimal = new Cat();
        myAnimal.doEat();   // Output: Cat is eating
        myAnimal.doSound(); // Output: Cat meows

        // Creating Animal reference for Dog object
        myAnimal = new Dog();
        myAnimal.doEat();   // Output: Dog is eating
        myAnimal.doSound(); // Output: Dog barks
    }
}
  • Base Class Animal: Defines the methods doEat() and doSound(), providing default behaviors.

  • Derived Class Cat: Overrides doEat() and doSound() to provide specific behaviors for a cat.

  • Derived Class Dog: Overrides doEat() and doSound() to provide specific behaviors for a dog.

  • Polymorphism in Action: In the main method, an Animal reference is used to point to Cat and Dog objects. Despite the reference being of type Animal, the overridden methods in Cat and Dog are called, demonstrating polymorphism.

This program showcases how different objects (cat and dog) of the same base class (Animal) can exhibit different behaviors based on their specific implementations of the overridden methods.

This process of creating child class objects and assigning them to a parent class reference variable is a demonstration of polymorphism.

Types of Polymorphism

There are two types of polymorphism in java:

  1. Compile-time Polymorphism/Static Polymorphism.

  2. Run-time Polymorphism/Dynamic Polymorphism.

Compile-time polymorphism

Compile-time polymorphism also known as static polymorphism, is achieved through method overloading. Method overloading allows a programmer to create multiple methods of same name but different type of parameters in same class.

public class MethodOverloading {

    public static void main(String[] args) {
        // Calling the overloaded methods with different parameters
        doTea();
        doTea("Masala Tea", 2);
        doTea("Ginger", 3, 15.45f);
        doTea("Green Tea", 1, "paper glass");
    }

    // Method with no parameters
    public static void doTea() {
        System.out.println("Prepare normal tea.");
    }

    // Overloaded method with two parameters
    public static void doTea(String type, int no) {
        System.out.println("Prepare " + type + " in " + no + " servings.");
    }

    // Overloaded method with three parameters including a float
    public static void doTea(String type, int no, float price) {
        System.out.println("Prepare " + type + " in " + no + " servings, costing around " + price + " each.");
    }

    // Overloaded method with three parameters including a string
    public static void doTea(String type, int no, String glass) {
        System.out.println("Prepare " + type + " in " + no + " servings and serve it in a " + glass + ".");
    }
}

This program demonstrates method overloading, which is a form of polymorphism in Java where multiple methods have the same name but different parameter lists. The compiler determines which method to call based on the method signature (the number and type of parameters).

Method without Parameters:

public static void doTea() {
    System.out.println("Prepare normal tea.");
}
  • This method takes no arguments and prints a message indicating the preparation of normal tea.

Method with Two Parameters:

public static void doTea(String type, int no) {
    System.out.println("Prepare " + type + " in " + no + " servings.");
}
  • This method takes a String (type of tea) and an int (number of servings) as arguments and prints a message with these details.

Method with Three Parameters (including a float):

public static void doTea(String type, int no, float price) {
    System.out.println("Prepare " + type + " in " + no + " servings, costing around " + price + " each.");
}
  • This method takes a String (type of tea), an int (number of servings), and a float (price per serving) as arguments and prints a message with these details.

Method with Three Parameters (including a String):

public static void doTea(String type, int no, String glass) {
    System.out.println("Prepare " + type + " in " + no + " servings and serve it in a " + glass + ".");
}
  • This method takes a String (type of tea), an int (number of servings), and another String (type of glass) as arguments and prints a message with these details.

Main Method:

public static void main(String[] args) {
        // Calling the overloaded methods with different parameters
        doTea();
        doTea("Masala Tea", 2);
        doTea("Ginger", 3, 15.45f);
        doTea("Green Tea", 1, "paper glass");
    }

The main method demonstrates calling each version of the overloaded doTea method:

  • doTea(): Calls the method with no parameters, which prints "Prepare normal tea."

  • doTea("Masala Tea", 2): Calls the method with a String and an int, which prints "Prepare Masala Tea in 2 servings."

  • doTea("Ginger", 3, 15.45f): Calls the method with a String, an int, and a float, which prints "Prepare Ginger in 3 servings, costing around 15.45 each."

  • doTea("Green Tea", 1, "paper glass"): Calls the method with a String, an int, and another String, which prints "Prepare Green Tea in 1 serving and serve it in a paper glass."

Compile-Time Polymorphism: Method overloading is resolved at compile-time. When you call doTea() with different sets of parameters, the compiler determines which version of the doTea method to invoke based on the arguments provided.

Flexible Method Interface: By providing multiple methods with the same name but different parameters, you allow users of your class to call the doTea method in various ways depending on the information they have, making the interface flexible and easy to use.

In the main method, you can see how each overloaded method is called with different sets of arguments, demonstrating the concept of method overloading.

Run-time Polymorphism

Run-time polymorphism is achieved through method overriding. This happens when a subclass provides its own specific implementation of a method that is already defined in its superclass. Want to know more about method overriding feel free to read my blog on the topic of Complete Guide to Method Overriding in Java

Upcasting

Upcasting is the process of treating an object of a derived class as an object of its base class. It involves converting a reference to a derived class object to a reference of its base class type. Upcasting allows objects of different derived classes to be treated uniformly through a common base class interface.

Example

// Base class
class Animal {
    void doEat() {
        System.out.println("Animal is eating");
    }

    void doSound() {
        System.out.println("Animal makes a sound");
    }
}

// Derived class Cat
class Cat extends Animal {
    @Override
    void doEat() {
        System.out.println("Cat is eating fish");
    }

    @Override
    void doSound() {
        System.out.println("Cat meows");
    }
}

// Derived class Dog
class Dog extends Animal {
    @Override
    void doEat() {
        System.out.println("Dog is eating chicken");
    }

    @Override
    void doSound() {
        System.out.println("Dog barks");
    }
}

public class TestPolymorphism {
    public static void main(String[] args) {
        // Creating Animal reference for Cat object
        Animal myAnimal = new Cat();
        myAnimal.doEat();   // Output: Cat is eating
        myAnimal.doSound(); // Output: Cat meows

        // Creating Animal reference for Dog object
        myAnimal = new Dog();
        myAnimal.doEat();   // Output: Dog is eating
        myAnimal.doSound(); // Output: Dog barks
    }
}

Animal Reference: The reference variable myAnimal is of type Animal.

Assigning Subclass Objects:

  • myAnimal = new Cat(); assigns a Cat object to the Animal reference variable. This is upcasting, as a Cat object is being referenced by an Animal type.

  • myAnimal = new Dog(); assigns a Dog object to the Animal reference variable. This is also upcasting, as a Dog object is being referenced by an Animal type.

Method Calls:

  • When myAnimal.doEat(); and myAnimal.doSound(); are called, the actual method that gets executed is determined at runtime based on the object type (Cat or Dog). This is run-time polymorphism.

Upcasting is useful because it allows a single reference type (Animal in this case) to refer to objects of different subclasses (Cat and Dog). This enables polymorphic behavior where the correct method implementation (from the Cat or Dog class) is called based on the actual object type at runtime. This makes the code more flexible and extensible.

Limitations of upcasting

By using parent type reference the child specific methods cannot be accessed or invoke directly although we can access the child specific by performing downcasting.

Downcasting

Downcasting is a process of converting the parent type reference to the child object for calling the child specific methods using the parent type reference.

// Base class
class Animal {
    // Method to produce sound
    public void doSound() {
        System.out.println("Animal is making sound");
    }
}

// Derived class Cat
class Cat extends Animal {
    @Override
    public void doSound() {
        System.out.println("Cat meows");
    }

    // Cat-specific method
    public void doActivity() {
        System.out.println("Cat catches Mouse");
    }
}

// Derived class Dog
class Dog extends Animal {
    @Override
    public void doSound() {
        System.out.println("Dog Barks");
    }

    // Dog-specific method
    public void doActivity() {
        System.out.println("Dog Guards");
    }
}

public class MethodOverloading {
    public static void main(String[] args) {
        // Animal reference pointing to a Cat object (upcasting)
        Animal animal = new Cat();
        animal.doSound(); // Calls the overridden method in Cat class: "Cat meows"

        // Downcasting to access the Cat-specific method
        ((Cat) animal).doActivity(); // Calls the Cat-specific method: "Cat catches Mouse"

        // Animal reference pointing to a Dog object (upcasting)
        animal = new Dog();
        animal.doSound(); // Calls the overridden method in Dog class: "Dog Barks"

        // Downcasting to access the Dog-specific method
        ((Dog) animal).doActivity(); // Calls the Dog-specific method: "Dog Guards"
    }
}
  1. Class Definitions:

    • Animal: This is the base class with a method doSound() that prints a generic message indicating an animal is making a sound.

    • Cat: This subclass of Animal overrides the doSound() method to print "Cat meows" and adds a doActivity() method that prints "Cat catches Mouse."

    • Dog: This subclass of Animal overrides the doSound() method to print "Dog Barks" and adds a doActivity() method that prints "Dog Guards."

  2. Upcasting:

    • In the main method, an Animal reference variable animal is created. First, it is assigned a new Cat object. This process is called upcasting, where a subclass object (in this case, Cat) is referred to by a superclass reference (in this case, Animal).
  3. Calling Overridden Methods:

    • The doSound() method is called on the animal reference. Since animal currently refers to a Cat object, the overridden doSound() method in the Cat class is executed, printing "Cat meows."
  4. Downcasting:

    • To access the Cat-specific method doActivity(), the animal reference is explicitly cast to a Cat. This is known as downcasting. After downcasting, the doActivity() method of the Cat class is called, printing "Cat catches Mouse."
  5. Reassigning and Repeating for Dog:

    • The animal reference is then reassigned to a new Dog object, again demonstrating upcasting. The doSound() method is called on the animal reference, and since animal now refers to a Dog object, the overridden doSound() method in the Dog class is executed, printing "Dog Barks."
  6. Downcasting for Dog:

    • Similar to the previous downcasting, the animal reference is explicitly cast to a Dog to access the Dog-specific method doActivity(). After downcasting, the doActivity() method of the Dog class is called, printing "Dog Guards."

Output

Conclusion

In conclusion, polymorphism in Java is a powerful concept that enhances code flexibility and reusability in object-oriented programming. It allows different classes to be treated as instances of their superclass, promoting code efficiency and organization through inheritance and method overriding.

We've explored how polymorphism works through both compile-time and run-time examples. Compile-time polymorphism, achieved via method overloading, enables the same method name to perform different tasks based on the arguments passed. On the other hand, run-time polymorphism, achieved through method overriding and supported by upcasting and downcasting, allows a subclass to provide its specific implementation of methods defined in its superclass.

Understanding and effectively implementing polymorphism not only simplifies code maintenance but also fosters the creation of robust and adaptable software solutions. By leveraging polymorphism, Java developers can write cleaner, more concise code that is easier to extend and maintain over time.

In essence, mastering polymorphism empowers Java developers to write efficient, scalable, and maintainable code, making it a fundamental pillar of object-oriented programming.

1
Subscribe to my newsletter

Read articles from Sridhar K directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sridhar K
Sridhar K

I am a fresher Software developer with a strong foundation in Java, HTML, CSS, and JavaScript. I have experience with various libraries and frameworks, including Spring Boot, Express.js, Node.js, Hibernate, MySQL, and PostgreSQL. I am eager to leverage my technical skills and knowledge to contribute effectively to a dynamic development team. I am committed to continuous learning and am excited to begin my career in a challenging developer role.