</>waridocu

Java

Polimorfismo y clases abstractas

Por qué una variable de tipo padre puede guardar objetos hijos, cómo Java decide qué versión de un método ejecutar, y para qué sirve una clase abstract.

javapolimorfismoabstractherenciainstanceofpoo

El problema: un if por cada tipo

Con Alumno y Profesor heredando de Persona, supón que hay que imprimir una lista mezclada. Sin polimorfismo, el código se ve así:

Ejemplo: el código que el polimorfismo viene a eliminarjava
for (Object o : lista) {
    if (o instanceof Alumno) {
        System.out.println(((Alumno) o).describirAlumno());
    } else if (o instanceof Profesor) {
        System.out.println(((Profesor) o).describirProfesor());
    }
}

Ese if crece con cada tipo nuevo, y hay que acordarse de tocarlo en todos los lugares donde aparezca. El polimorfismo permite borrarlo entero.

Una variable del tipo padre, un objeto del tipo hijo

En Java, una variable declarada con el tipo del padre puede guardar cualquier objeto de una clase hija:

javajava
Persona p1 = new Alumno("Ana", 20, 8.5);
Persona p2 = new Profesor("Luis", 45, "Historia");

Esto es legal porque todo Alumno es una Persona — la relación que la herencia establece se lee literalmente así, “es un/a”. Y como los dos son Persona, caben en la misma lista:

Ejemplo: una sola lista, un solo bucle, sin ifsjava
ArrayList<Persona> gente = new ArrayList<>();
gente.add(new Alumno("Ana", 20, 8.5));
gente.add(new Profesor("Luis", 45, "Historia"));
gente.add(new Alumno("Beto", 22, 6.0));

for (Persona p : gente) {
    System.out.println(p.describir());
}
texttext
Ana, 20 años — nota: 8.5
Luis, 45 años — dicta Historia
Beto, 22 años — nota: 6.0

La línea p.describir() es una sola y produce tres salidas distintas. De ahí el nombre: poli (“muchas”) + morfo (“formas”) — una misma llamada toma muchas formas según qué objeto la reciba.

La regla que explica todo

Este es el mecanismo, en dos frases que conviene aprender de memoria:

El tipo de la variable decide qué métodos puedes escribir (qué compila). El tipo real del objeto decide qué versión se ejecuta.

javajava
Persona p = new Alumno("Ana", 20, 8.5);

p.describir();   // ✅ compila: Persona tiene describir()
                 //    ejecuta la versión de Alumno
p.getNota();     // ❌ NO compila: Persona no tiene getNota()

Que p.getNota() no compile sorprende al principio — el objeto tiene ese método. Pero el compilador solo mira el tipo declarado (Persona), y desde ahí no hay garantía de que el objeto sea un Alumno; podría ser un Profesor. A ese mecanismo de elegir la implementación en tiempo de ejecución se lo llama dynamic dispatch o ligadura dinámica.

Volver al tipo hijo: instanceof y el cast

Cuando genuinamente necesitas lo específico del hijo, hay que comprobar el tipo y convertir:

Ejemplo: bajar de Persona a Alumno de forma segurajava
for (Persona p : gente) {
    if (p instanceof Alumno alumno) {
        System.out.println("Nota: " + alumno.getNota());
    }
}

instanceof se traduce como “es una instancia de”, y devuelve true/false. Desde Java 16 se le puede pegar un nombre de variable (instanceof Alumno alumno) y Java hace la conversión por ti — se llama pattern matching. En código más viejo verás la forma en dos pasos:

javajava
if (p instanceof Alumno) {
    Alumno alumno = (Alumno) p;
    System.out.println("Nota: " + alumno.getNota());
}
Advertencia

Hacer el cast sin preguntar con instanceof compila igual, pero si el objeto no es de ese tipo el programa revienta en ejecución con ClassCastException. Y si estás escribiendo muchos instanceof, suele ser señal de que el comportamiento debería vivir como un método sobrescrito en cada clase — que es exactamente el if que este capítulo vino a eliminar.

Clases abstractas: un padre que no se puede instanciar

Hay un problema de diseño en Persona: nada impide escribir new Persona("X", 30). Pero una “persona” que no es ni alumno ni profesor no significa nada en este sistema, y su describir() genérico existe solo para ser reemplazado.

abstract se traduce como “abstracto”: algo que existe como idea, no como cosa concreta. Una clase abstract sirve de padre pero no se puede instanciar:

Ejemplo: Persona como clase abstractajava
public abstract class Persona {
    protected String nombre;
    protected int edad;

    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }

    // Método concreto: lo heredan todas las hijas tal cual
    public String getNombre() {
        return nombre;
    }

    // Método abstracto: sin cuerpo. Cada hija DEBE escribirlo.
    public abstract String describir();
}
javajava
Persona p = new Persona("X", 30);   // ❌ error de compilación
Persona p = new Alumno("Ana", 20, 8.5);   // ✅ correcto

El error dice Persona is abstract; cannot be instantiated. Y sin embargo Persona sigue siendo un tipo perfectamente válido para declarar variables, parámetros y listas — que es todo lo que el polimorfismo necesitaba.

Método abstracto: firma sin cuerpo

public abstract String describir(); termina en ;, sin llaves. No dice cómo describir a alguien; dice que toda hija tiene que saber hacerlo. Si Alumno se olvida de implementarlo, no compila.

Compara las dos formas de heredar un comportamiento:

Método normal en el padreMétodo abstract en el padre
¿Tiene cuerpo?No, termina en ;
La hija, ¿debe sobrescribirlo?No, es opcionalSí, es obligatorio
¿Qué pasa si no lo hace?Hereda la versión del padreError de compilación
Sirve paraComportamiento compartido que a veces se ajustaComportamiento que solo la hija puede definir
Nota

Una clase abstract puede mezclar los dos libremente: atributos, constructores, métodos con cuerpo y métodos abstractos. Eso es justamente lo que la distingue de una interfaz, que tradicionalmente solo aportaba las firmas.

Importante

Si una clase tiene aunque sea un método abstract, la clase entera tiene que declararse abstract. Al revés no: una clase puede ser abstract sin tener ningún método abstracto, simplemente para prohibir que se la instancie.

El ejemplo completo

Ejemplo: figuras geométricas, el caso clásicojava
public abstract class Figura {
    private String nombre;

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

    public String getNombre() {
        return nombre;
    }

    public abstract double area();

    public String resumen() {
        return nombre + ": área " + area();
    }
}
javajava
public class Circulo extends Figura {
    private double radio;

    public Circulo(double radio) {
        super("Círculo");
        this.radio = radio;
    }

    @Override
    public double area() {
        return Math.PI * radio * radio;
    }
}

public class Rectangulo extends Figura {
    private double ancho, alto;

    public Rectangulo(double ancho, double alto) {
        super("Rectángulo");
        this.ancho = ancho;
        this.alto = alto;
    }

    @Override
    public double area() {
        return ancho * alto;
    }
}
javajava
Figura[] figuras = { new Circulo(2), new Rectangulo(3, 4) };
for (Figura f : figuras) {
    System.out.println(f.resumen());
}

Fíjate en lo que hace resumen(): es un método concreto, escrito una sola vez en la clase abstracta, que llama a area(), un método abstracto. Es decir, código del padre llamando a código que todavía no existe y que cada hija va a aportar. Agregar un Triangulo mañana no obliga a tocar resumen() ni el bucle: solo escribir la clase nueva.

Pon a prueba

🎯 ¿Cuál de estas líneas NO compila?
  • Compila. Guardar un objeto hijo en una variable de tipo padre es precisamente lo que habilita el polimorfismo, y no necesita cast.

  • Correcto: Figura es abstract, así que no se puede instanciar. El compilador responde Figura is abstract; cannot be instantiated. Sí se puede usar Figura como tipo de variable, que es lo que hacen las otras opciones.

  • Compila. Un array de Figura acepta cualquier objeto que sea una Figura, y tanto Circulo como Rectangulo lo son.

  • Compila. resumen() es un método concreto heredado de Figura, disponible en cualquier hija sin necesidad de reescribirlo.

💡 Predice la salida

Con las clases Figura, Circulo y Rectangulo de arriba, ¿qué imprime este programa?

javajava
Figura f = new Rectangulo(3, 4);
System.out.println(f.area());
System.out.println(f.getNombre());
System.out.println(f instanceof Circulo);
System.out.println(f instanceof Figura);

Imprime 12.0, Rectángulo, false y true.

  • f.area() da 12.0: la variable es de tipo Figura, pero el objeto real es un Rectangulo, así que se ejecuta su versión de area() (3 * 4). Este es el corazón del polimorfismo.
  • f.getNombre() da Rectángulo porque el constructor de Rectangulo le pasó ese texto al padre con super("Rectángulo").
  • f instanceof Circulo es false: el objeto es un rectángulo, y Circulo no está en su línea de herencia.
  • f instanceof Figura es true: instanceof responde true también para las clases padre, no solo para el tipo exacto.
✏️ Completa la clase abstracta

Vehiculo no debe poder instanciarse y obliga a cada hija a definir cuántas ruedas tiene, pero sí trae una descripción ya escrita.

public  class Vehiculo {
  private String marca;

  public Vehiculo(String marca) {
      this.marca = marca;
  }

  public abstract int ruedas()

  public String describir() {
      return marca + " con " + () + " ruedas";
  }
}

🧩 Ordena el código

Ordena las líneas para escribir una hija concreta de Vehiculo que implemente el método abstracto.

  1. }
  2. public int ruedas() {
  3. public Moto(String marca) {
  4. @Override
  5. public class Moto extends Vehiculo {
  6. super(marca);
  7. }
  8. }
  9. return 2;

Siguiente paso

Una clase abstracta te da un padre común, pero Java solo permite extends una vez. Cuando lo que necesitas es que clases sin ningún parentesco entre sí compartan un mismo contrato — “esto se puede comparar”, “esto se puede dibujar” — la herramienta es otra: ver Interfaces.