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.
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í:
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:
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:
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());
}Ana, 20 años — nota: 8.5
Luis, 45 años — dicta Historia
Beto, 22 años — nota: 6.0La 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.
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 sí 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:
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:
if (p instanceof Alumno) {
Alumno alumno = (Alumno) p;
System.out.println("Nota: " + alumno.getNota());
}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:
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();
}Persona p = new Persona("X", 30); // ❌ error de compilación
Persona p = new Alumno("Ana", 20, 8.5); // ✅ correctoEl 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 padre | Método abstract en el padre | |
|---|---|---|
| ¿Tiene cuerpo? | Sí | No, termina en ; |
| La hija, ¿debe sobrescribirlo? | No, es opcional | Sí, es obligatorio |
| ¿Qué pasa si no lo hace? | Hereda la versión del padre | Error de compilación |
| Sirve para | Comportamiento compartido que a veces se ajusta | Comportamiento que solo la hija puede definir |
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.
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
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();
}
}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;
}
}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
Con las clases Figura, Circulo y Rectangulo de arriba, ¿qué imprime este programa?
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()da12.0: la variable es de tipoFigura, pero el objeto real es unRectangulo, así que se ejecuta su versión dearea()(3 * 4). Este es el corazón del polimorfismo.f.getNombre()daRectánguloporque el constructor deRectangulole pasó ese texto al padre consuper("Rectángulo").f instanceof Circuloesfalse: el objeto es un rectángulo, yCirculono está en su línea de herencia.f instanceof Figuraestrue:instanceofrespondetruetambién para las clases padre, no solo para el tipo exacto.
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 las líneas para escribir una hija concreta de Vehiculo que implemente el método abstracto.
- }
- public int ruedas() {
- public Moto(String marca) {
- @Override
- public class Moto extends Vehiculo {
- super(marca);
- }
- }
- 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.