OOP-101: Polymorphism – What is it, why use it, and an example

OOP-101: Polymorphism – What is it, why use it, and an example

In the world of object-oriented programming (OOP), polymorphism is a powerful concept that allows for flexibility and code reusability. Essentially, polymorphism means “having many forms.” (Poly meaning ‘many’ as opposed to ‘mono’, meaning one) In programming, it translates to the ability of code to work with objects of different types while treating them through a single interface.

Imagine having a remote control for your entertainment system. You can press the “play” button regardless of whether you’re playing a DVD or Blueray, streaming a movie, or listening to music. The remote acts as the interface, and the specific devices receiving the command handle the action in their own way (They are the objects). Polymorphism works similarly.

Why is Polymorphism Important?

Polymorphism offers several advantages:

  • Flexibility: Code can adapt to different object types without needing to be rewritten for each specific case.
  • Maintainability: As your program evolves, adding new object types becomes easier since the core logic remains unchanged.
  • Readability: Code that leverages polymorphism is often cleaner and easier to understand because it focuses on the functionality rather than the specifics of each object type.

A Use Case: Animal Sounds

Let’s consider a scenario where you want to create a program that simulates animal sounds. You can define an abstract base class Animal with a method makeSound() that makes a generic animal noise. Now, create derived classes like Dog, Cat, and Bird that inherit from Animal and override the makeSound() method to produce their unique sounds (bark, meow, chirp, etc.).

// Base class
public class Animal {
  public void makeSound();
}

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

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

// Derived class - Bird
class Bird extends Animal {
  @Override
  public void makeSound() {
    System.out.println("Chirp!");
  }
}

public class AnimalSoundSimulator {
  public static void main(String[] args) {
    Animal[] animals = {new Dog(), new Cat(), new Bird()};
    for (Animal animal : animals) {
      animal.makeSound();
    }
  }
}

The above example is somewhat contrived and simplistic in nature, but it does convey the core concepts of polymorphism fairly well.

We have a base class of Animal, which defined a makeSound() method, and then several classes that extend the base class and then overwrite the behavior of the makeSound() method.

The animalSoundSimulator class simply defined a new collection of Animal objects (Since they all extend the Animal class, we can consider them to be Animals), and then loop over the collection and invoke makeSound() on each animal to see polymorphism in action.

Bonus: Extends vs. Implements

You’ll notice in the above that all of the classes like Cat, Dog, and Bird all extend the Animal class. This means that Cat, Dog, and Bird all, at a minimum, have at least the functionality of the class that they are extending, and in order for the magic to take place, you must override the methods that are defined in the class that is being extended. (The @Override annotation ensures that the method is being truly overwritten and not just overloaded, in which case strange behavior might occur and it might be difficult to debug.)

However, when it comes to implementing an Interface, the classes that are implementing the interface must conform to the methods outlined in the interface, however Overriding the methods defined in the interface is not a requirement. Also note that in the case of implementing an interface, the class that is doing the implementing must implement all of the behavior defined in the interface itself – Think of it as a contract of behavior.