Java tutorials > Core Java Fundamentals > Object-Oriented Programming (OOP) > What is polymorphism and how is it achieved?

What is polymorphism and how is it achieved?

Polymorphism, derived from the Greek words 'poly' (many) and 'morph' (form), is one of the fundamental concepts of object-oriented programming (OOP). It refers to the ability of an object to take on many forms. More specifically, it allows you to treat objects of different classes in a uniform way. In Java, polymorphism is achieved through method overloading and method overriding.

Understanding Polymorphism

Polymorphism allows you to write code that can work with objects of different classes without knowing their specific types at compile time. This makes your code more flexible, reusable, and maintainable. Java supports two main types of polymorphism: compile-time polymorphism (method overloading) and runtime polymorphism (method overriding).

Compile-Time Polymorphism (Method Overloading)

Method overloading occurs when a class has multiple methods with the same name but different parameter lists (different number of parameters, different types of parameters, or different order of parameters). The compiler determines which method to call based on the arguments passed to the method during compile time. This is also known as static polymorphism or early binding.

In the example above, the Calculator class has three add methods. The compiler chooses the appropriate add method based on the number and types of arguments provided.

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

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

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

    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(2, 3));        // Output: 5
        System.out.println(calc.add(2, 3, 4));     // Output: 9
        System.out.println(calc.add(2.5, 3.5));   // Output: 6.0
    }
}

Runtime Polymorphism (Method Overriding)

Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its superclass. The method in the subclass must have the same name, return type, and parameter list as the method in the superclass. The @Override annotation is used to indicate that a method is intended to override a method in the superclass. The actual method to be executed is determined at runtime based on the object's actual type. This is also known as dynamic polymorphism or late binding.

In the example above, the Dog and Cat classes override the makeSound method of the Animal class. When makeSound is called on a Dog object, the Dog's version of the method is executed, and similarly for the Cat object.

class Animal {
    public void makeSound() {
        System.out.println("Generic animal sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Animal dog = new Dog();
        Animal cat = new Cat();

        animal.makeSound(); // Output: Generic animal sound
        dog.makeSound();    // Output: Woof!
        cat.makeSound();    // Output: Meow!

        Animal[] animals = new Animal[3];
        animals[0] = new Animal();
        animals[1] = new Dog();
        animals[2] = new Cat();

        for (Animal a : animals) {
            a.makeSound();
        }
    }
}

Concepts Behind the Snippets

The core concept behind polymorphism is the ability to treat different objects in a uniform manner through a common interface (e.g., a superclass). This is achieved through inheritance and interfaces. Inheritance enables subclasses to inherit behavior and state from superclasses, while interfaces define contracts that classes can implement. Polymorphism relies on the Liskov Substitution Principle, which states that subtypes should be substitutable for their base types without altering the correctness of the program.

Real-Life Use Case

Consider a drawing application that can draw different shapes (e.g., circles, rectangles, triangles). You can define an abstract class or interface called Shape with a draw() method. Each shape class (Circle, Rectangle, Triangle) would then implement the Shape interface and provide its own implementation of the draw() method. The application can then store all shapes in a list of Shape objects and iterate through the list, calling the draw() method on each object. The appropriate draw() method will be called based on the actual type of the object, demonstrating polymorphism.

Best Practices

  • Use polymorphism to create flexible and extensible code.
  • Favor composition over inheritance when possible.
  • Adhere to the Liskov Substitution Principle to ensure that subtypes are substitutable for their base types.
  • Use interfaces to define contracts and promote loose coupling.

Interview Tip

When discussing polymorphism in an interview, be sure to explain both compile-time (method overloading) and runtime (method overriding) polymorphism. Provide clear examples of each and explain how they contribute to code flexibility and reusability. You can also discuss the benefits of using interfaces and abstract classes to achieve polymorphism.

When to Use Polymorphism

Use polymorphism when you want to create code that can work with objects of different classes in a uniform way. It's especially useful when you have a hierarchy of classes with common behavior but different implementations. Polymorphism promotes code reuse and reduces code duplication.

Memory Footprint

Polymorphism itself does not significantly increase the memory footprint. However, using inheritance can increase the memory footprint of objects because subclasses inherit the fields of their superclasses. The v-table (virtual method table) used for dynamic dispatch in runtime polymorphism also adds a small overhead. However, the benefits of polymorphism in terms of code organization and maintainability usually outweigh the slight increase in memory consumption.

Alternatives

While polymorphism is a powerful tool, there are alternatives in some situations. Function pointers (in languages like C++) can be used to achieve similar results, but they are less type-safe. Conditional statements (e.g., switch statements) can also be used, but this approach can lead to less maintainable and less extensible code compared to polymorphism.

Pros

  • Increased code reusability
  • Improved code maintainability
  • Enhanced code flexibility
  • Reduced code duplication

Cons

  • Can increase code complexity if not used carefully
  • Slight performance overhead due to dynamic dispatch (runtime polymorphism)

FAQ

  • What is the difference between method overloading and method overriding?

    Method overloading occurs within a single class when you have multiple methods with the same name but different parameter lists. Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its superclass.

  • What is the Liskov Substitution Principle?

    The Liskov Substitution Principle states that subtypes should be substitutable for their base types without altering the correctness of the program. This is a key principle for ensuring that polymorphism works correctly.

  • Why is polymorphism important in object-oriented programming?

    Polymorphism is important because it allows you to write code that can work with objects of different classes in a uniform way. This makes your code more flexible, reusable, and maintainable.