Saltar a contenido

Polimorfismo

El Polimorfismo es uno de los 4 pilares de la programación orientada a objetos (POO) junto con la Abstracción, Encapsulación y Herencia. Para entender que es el polimorfismo es muy importante que tengáis bastante claro el concepto de la Herencia.

Polimorfismo significa "que tiene muchas formas", es la capacidad que tienen los objetos de una clase en ofrecer respuesta distinta e independiente en función de los parámetros (diferentes implementaciones) utilizados durante su invocación.

Veamos un ejemplo para entender de forma más clara cómo funciona el polimorfismo.

Queremos crear un juego en la que tenemos animales de diferentes especies y que puedan hablar cada una en su propio lenguaje de su especie.

polimorfismo

El análisis del problema puede darnos un diagrama de clases como el que sigue

polimorfismo

Cada animal tiene su nombre y tenemos un método hablar() que cada especie tiene que implementar.

Crearemos unas clases que heredan de la clase padre Animal:

class Animal {
    private String nombre;

    public Animal(String nombre) {
        this.nombre = nombre;
    }

    public void hablar() {
        System.out.println( nombre+ " dice: ....");
    }

    public String getNombre() {
        return nombre;
    }
}

class Gato extends Animal {
    public Gato(String nombre) {
        super(nombre);
    }

    @Override
    public void hablar() {
        System.out.println( getNombre()+ " dice: Miaaauuuuuu....");
    }
}

class Perro extends Animal {
    public Perro(String nombre) {
        super(nombre);
    }

    @Override
    public void hablar() {
        System.out.println( getNombre()+ " dice: Guuuaaaaaaauuuuu.....");
    }
}

class Persona extends Animal {
    public Persona(String nombre) {
        super(nombre);
    }

    @Override
    public void hablar() {
        System.out.println( getNombre()+ " dice: Blablablabla....");
    }
}
Cada uno de los animales habla su propio idioma al sobrescribir el método hablar()

En nuestro fantástico juego, queremos que los animales de la habitación tengan una conversación, el código puede ser algo parecido a los siguiente.

public static void main(String[] args) {
    Gato gato1=new Gato("Garfiel");
    Perro perro1=new Perro("Laika");
    Persona persona1=new Persona("Pirico");
    //conversar
    gato1.hablar();
    perro1.hablar();
    persona1.hablar();
}
Cada uno de los personajes hablará en su idioma .

Garfield  dice: Miaaauuuuuu....
Laika dice: Guuuaaaaaaauuuuu.....
Perico dice: Blablablabla....

Pero en nuestro juego no sabemos ni cuantos animales ni el tipo de animal que pueden haber en la habitación, todo depende del desarrollo del juego por parte del jugador. Pero tenemos que hacer que todos los animales de la habitación hablen.

La potencia del polimorfismo nos permite tener en una variable del tipo de la clase padre, en nuestro caso Animal una instancia de las clases hijas y cuando llamamos al método hablar() llamará al método de la clase hija.

    Animal animal=new Gato("Garfiel");
    animal.hablar();
muestra

Garfield  dice: Miaaauuuuuu....

Esto nos permite una solución más abierta manteniendo un array de tipo Animal, donde guardamos los diferentes animales y recorremos para que hablen todos los animales, cada uno en su idioma.

public static void main(String[] args) {
        //creamos un array de tipo Animal
        Animal[] animales = {
                new Gato("Garfiel"),
                new Perro("Laika"),
                new Persona("Pirico"),
                new Gato("Misifu"),
                new Perro("Goofy")
        };
        //Conversanción de animales
        for (Animal animal : animales) {
            //llamará al método correspondiente del tipo de la instancia
            animal.hablar();
        }
    }
Garfiel dice: Miaaauuuuuu....
Laika dice: Guuuaaaaaaauuuuu.....
Pirico dice: Blablablabla....
Misifu dice: Miaaauuuuuu....
Goofy dice: Guuuaaaaaaauuuuu.....
polimorfismo

De esta forma, podríamos añadir en un futuro nuevos tipos de animal y que no afectara al código del método conversar().

Animal[] animales = {
                //nuevo animal
                new Raton("Mickey"),
                new Gato("Garfiel"),
                new Perro("Laika"),
                new Persona("Pirico"),
                new Gato("Misifu"),
                new Perro("Goofy")
        };
//***********************************************
//******el código principal no se ve afectado****
//***********************************************

//Conversanción de animales
        for (Animal animal : animales) {
            //llamará al método correspondiente del tipo de la instancia
            animal.hablar();
        }

Resumen

La sobrescritura de métodos y las conversiones entre clases de la jerarquía sientan las bases para el polimorfismo. Es necesario entender bien estos conceptos para comprender el polimorfismo. Este se puede definir como la cualidad que tienen los objetos para responder de distinto modo a un mismo mensaje.

El programa tiene que cumplir

  • Los métodos deben estar declarados (métodos abstractos) y a veces también pueden estar implementados (métodos no abstractos) en la clase base.
  • Los métodos deben estar redefinidos en las clases derivadas.
  • Los objetos deben ser manipulados utilizando referencias a la clase base.

Upcasting, Downcasting

Una variable puede contener una referencia a un objeto cuya clase es descendiente de la clase de la variable. Ejemplo:

    //La variable animal es una referencia a un objeto Perro que es descendiente de Animal. 
    Animal animal = new Perro();

Un descendiente de una clase es un hijo de esa clase, o un hijo de un hijo de esa clase, y así sucesivamente. Los hermanos no son descendientes entre sí.

NO podemos asignar un objeto de referencia de padre a una variable de clase hijo (Perro p = new Animal()). Si queremos convertir un padre en hijo, la variable tiene que ser creada de tipo hijo. Si queremos convertir un hijo en padre tendremos que hacer un Upcasting, y al revés tendríamos un Downcasting:

Polimorfismo

Ejemplo

    //NO SE PUEDE HACER: ERROR
    Perro p = new Animal(); //no compila
    Perro p = (Perro) new Animal(); // compila pero da error de ejecución

    //DOWNCASTING, convertir padre en hijo
    Animal a = new Perro("Tobi");
    Perro pe = (Perro)a;

    //UPCASTING, convertir hijo en padre
    Animal a2 = (Animal) new Perro("Milu");
    //no es necesario hacer cast
    Animal a3 = new Perro("Niebla");

El operador instanceof

Las operaciones entre clases y en particular el downcasting requieren que las clases sean de tipos compatibles. Para asegurarnos de ello podemos utilizar el operador instanceof. Por otro lado, podemos tener la necesidad de tomar decisiones en función del tipo del objeto

instanceof devuelve true si el objeto es instancia de la clase y false en caso contrario. La sintaxis es:

Objecto instanceof Clase

Animal animal=new Gato("ConBotas");
if (animal instanceof Gato) {
    queSoy = "soy un gato";
} else if (animal instanceof Perro) {
    queSoy = "soy un perro";
} else if (animal instanceof Persona) {
    queSoy = "soy una persona";
} else {
    queSoy = "no se que quien soy";
}
Un ejemplo más claro con varios animales en el que tenemos que tomar decisiones en función del tipo de animal

// 1. Crear el array de Animales
Animal[] animales = new Animal[4];
animales[0] = new Gato("ConBotas");
animales[1] = new Perro("Scooby");
animales[2] = new Persona("Ana");
animales[3] = new Gato("Félix");

System.out.println("--- Procesando el array de animales ---");

// 2. Recorrer el array usando un bucle for-each
for (Animal animal : animales) {
    String queSoy;
    String nombre = animal.getNombre(); 

    // 3. Evaluar el tipo usando if/else if (instanceof)
    if (animal instanceof Gato) {
        queSoy = "soy un gato";
    } else if (animal instanceof Perro) {
        queSoy = "soy un perro";
    } else if (animal instanceof Persona) {
        queSoy = "soy una persona";
    } else {
        queSoy = "no se que quien soy";
    }

    System.out.println(nombre + ": " + queSoy);
}

Nuevo switch de java

El nuevo switch de java nos permite tomar decisiones en función del tipo de objeto. Es una opción mucha más clara que un if-else.

String queSoy=switch(animal){
    case Gato g -> "soy un gato";
    case Perro p -> "soy un perro";
    case Persona p -> "soy una persona";
    default ->  "no se que quien soy";
}

Este switch lo podéis usar en vuestros proyectos desde java 14 y con la característica anterior, desde java 17

Ejemplo de polimorfismo de Minecraft

La capacidad del polimorfismo de la POO permite en los videojuegos programar la gran dificultad de manejar la variedad de los diferentes personajes, pantallas, posibles ampliaciones futuras ...

Polimorfismo

Vamos a ver un ejemplo básico del famoso videojuego Minecraft en el que simulamos la programación de la reacción ante la destrucción individual de un bloque, en el que cada tipo de bloque tiene una reacción diferente

Un posible diseño puede ser el siguiente

Polimorfismo

Definimos un método abstracto destruir que tendrá que ser sobrescrito en cada uno de las nuevas clases de bloques que crearemos

Cada nueva bloque proporciona una implementación específica del método destruir() para simular el comportamiento de los bloques en Minecraft.

Polimorfismo: En el bucle for del método main(), se llama al método destruir() de cada objeto, y Java selecciona automáticamente la implementación correcta basada en el tipo del objeto.

// Clase base Bloque
abstract class Bloque {
    public abstract void destruir(); // Método abstracto para destruir un bloque
}

// Subclase Tierra
class Tierra extends Bloque {
    @Override
    public void destruir() {
        System.out.println("Has destruido un bloque de Tierra. Se convierte en tierra suelta.");
    }
}

// Subclase Piedra
class Piedra extends Bloque {
    @Override
    public void destruir() {
        System.out.println("Has destruido un bloque de Piedra. Se convierte en adoquín.");
    }
}

// Subclase Diamante
class Diamante extends Bloque {
    @Override
    public void destruir() {
        System.out.println("Has destruido un bloque de Diamante. ¡Obtienes un diamante!");
    }
}

// Subclase Madera
class Madera extends Bloque {
    @Override
    public void destruir() {
        System.out.println("Has destruido un bloque de Madera. Obtienes tablones.");
    }
}

// Clase principal
public class MinecraftPolimorfismo {
    public static void main(String[] args) {
        // Crear un array de bloques
        Bloque[] bloques = {
            new Tierra(),
            new Piedra(),
            new Diamante(),
            new Madera()
        };

        // Destruir cada bloque de forma polimórfica
        for (Bloque bloque : bloques) {
            bloque.destruir();
        }
    }
}

El polimorfismo nos permite, en nuestro caso, en una nueva actualización de Minecraft, añadir nuevos bloques sin que afecte al código principal. Por ejemplo, añadimos los bloques Arena y Lava

//  NUEVA SUBCLASE 1: Arena
class Arena extends Bloque {
    @Override
    public void destruir() {
        System.out.println("Has destruido un bloque de Arena. La arena cae debido a la gravedad.");
    }
}

//  NUEVA SUBCLASE 2: Lava
class Lava extends Bloque {
    @Override
    public void destruir() {
        System.out.println("Has destruido un bloque de Lava. ¡Te quemas! El flujo de lava se detiene temporalmente.");
    }
}
El juego no se ve afectado

// Destruir cada bloque de forma polimórfica
// La referencia 'bloque' siempre es de tipo Bloque, pero el método ejecutado
// depende del tipo de objeto real (Tierra, Piedra,  Arena, Lava...)
for (Bloque bloque : bloques) {
    bloque.destruir();
}

Resumen de ventajas clave del Polimorfismo

El polimorfismo permite que una única interfaz maneje objetos de diferentes tipos, ofreciendo beneficios clave:

  1. Simplificación y Claridad del Código

    • Interfaz Única: Permite usar un nombre de método común (destruir()) para múltiples implementaciones específicas.
    • Código Limpio: Se elimina la necesidad de grandes estructuras if/else if o switch basadas en el tipo de objeto (instanceof), simplificando la lógica.
    • Reusabilidad: El código que interactúa con la clase base (Bloque) sirve para todas las subclases futuras.
  2. Extensibilidad y Mantenibilidad

    • Fácil Expansión: Añadir un nuevo tipo de objeto (Arena) solo requiere crear la subclase e implementar el método.
    • Mínimo Impacto: El código ya existente (el bucle de recorrido) no necesita modificarse para manejar los nuevos tipos de objetos, haciendo el sistema más robusto.
  3. Flexibilidad y Desacoplamiento

    • Objetos Intercambiables: Los objetos de diferentes clases pueden tratarse de manera uniforme a través de la referencia de la clase base.
    • Baja Dependencia: El código cliente solo depende de la clase base (Bloque), no de los detalles de implementación específicos de cada subclase, creando sistemas modulares.