java-polimorfismo-overriding-sustitucion

Polymorphism in Java: Overriding and Substitution

  • 5 min

The polymorphism allows treating objects of different types through a common type while preserving their specific behavior.

We already know how to inherit (extends) and define abstract classes. Now we will see how different objects can respond differently to the same operation.

The word Polymorphism comes from the Greek “many forms”. In programming, it is the ability of an object to behave in different ways depending on the context, or rather, the ability of a reference variable to point to objects of different forms.

The Substitution Principle

To understand this, we must first engrave a concept: The Liskov Substitution Principle.

This principle basically says:

“If B is a child of A, then wherever you use an object of type A, you should be able to use an object of type B without the program failing.”

In Java, this translates to a variable of type PARENT can hold a reference to a CHILD object.

// Normal: Reference Coche -> Object Coche
Coche miCoche = new Coche();

// POLYMORPHISM (Upcasting): Reference Vehiculo -> Object Coche
Vehiculo miVehiculo = new Coche();
Copied!

This is legal because a Coche is a Vehículo. However, the reverse does not work. Not every vehicle is a car.

Coche c = new Vehiculo(); // ERROR! A generic vehicle is not a car.
Copied!

Reference Type vs. Object Type

This is where the mind usually “clicks” (or explodes). When we do:

Animal miMascota = new Perro();
Copied!

We have two actors:

  1. The Variable (miMascota): It is of type Animal. It is the “remote control”. It defines WHICH buttons you can press (which methods you can call).
  2. The Object (new Perro()): It is of type Perro. It is the actual “TV” in the Heap. It defines HOW it reacts when you press the button.
miMascota.hacerSonido(); // ✅ Works (Animal has that method)
// miMascota.traerLaPelota(); // ❌ COMPILATION ERROR
Copied!

Even though the actual object is a Perro and knows how to fetch the ball, the remote control is of type Animal, and generic animals do not fetch balls. The compiler only looks at the type of the variable (the left side of the equals sign).

Method Overriding

We already saw that a child inherits the methods of the parent. But what if it wants to change them? That is Overriding.

To override a method, we declare it again in the child class with the same signature (same name, same parameters).

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

public class Perro extends Animal {
    @Override // <--- Good practice
    public void hacerSonido() {
        System.out.println("Woof Woof!");
    }
}
Copied!

The @Override annotation Although it is not mandatory, ALWAYS use it. It tells the compiler: “Hey, my intention is to override a method from the parent. If I made a mistake in the name or parameters, warn me”.

Without it, you could accidentally create a new method instead of overriding one, driving you crazy trying to find the bug.

Dynamic Binding

Let’s see how the call is resolved in the polymorphic example:

Animal a = new Perro();
a.hacerSonido();
Copied!

What gets printed?

  1. “Generic sound…” (What the Animal variable says)?
  2. “Woof Woof!” (What the Perro object says)?

Answer: Woof Woof!

In Java, instance methods are polymorphic. Although the compiler checks the variable (to see if the method exists), the JVM at runtime looks for the method in the actual object in memory.

If the object is a Perro, it will execute the Perro version, even though the variable is Animal.

What is all this for?

You might think: “Why would I want to store a Perro in an Animal variable? If I store it in a Perro variable, I have access to everything.”

The power comes when working with Collections and generic methods. Imagine an array of animals:

// Polymorphic array: Stores Perros, Gatos, and any child of Animal
Animal[] zoologico = {
    new Perro(),
    new Gato(),
    new Perro(),
    new Leon()
};

for (Animal a : zoologico) {
    // We don't need to know the concrete subtype of 'a'.
    // Java will execute the correct method for each one.
    a.hacerSonido();
}
Copied!

Output:

Woof Woof!
Meow!
Woof Woof!
Grrrr!
Copied!

Without polymorphism, you would have to do a giant if: if (a is Perro) ladrar() else if (a is Gato) maullar().... Polymorphism eliminates those conditionals.

Object Casting (Downcasting)

Sometimes, you have an Animal and you are sure it’s a Perro inside, and you need to call traerLaPelota(). To recover the full reference, we do Casting (just like with (int) double).

Animal a = new Perro();
// a.traerLaPelota(); // Error

Perro p = (Perro) a; // Downcasting: "Trust me, this is a dog"
p.traerLaPelota();   // Now it works
Copied!

The Danger: ClassCastException

If you lie to the compiler, the program will crash at runtime.

Animal a = new Gato();
Perro p = (Perro) a; // BOOM! ClassCastException
// You cannot force a Gato into a Perro.
Copied!

To do it safely, use the instanceof operator:

if (a instanceof Perro) {
    Perro p = (Perro) a;
    p.traerLaPelota();
}
Copied!

Or the Pattern Matching version, which saves us from manual casting:

if (a instanceof Perro p) {
    p.traerLaPelota(); // 'p' is already created and cast automatically
}
Copied!