Saltar al contenido principal

Polimorfismo y clases abstractas

Si Circulo y Rectangulo son figuras, podemos tratarlos mediante el tipo común Figura sin perder su comportamiento particular. Esa capacidad es polimorfismo.

Una abstracción incompleta​

Todas las figuras pueden tener un color y calcular un área, pero no existe una fórmula general. Una clase abstracta reúne lo compartido y obliga a completar lo variable:

abstract class Figura {
private String color;

public Figura(String color) { this.color = color; }
public String getColor() { return color; }
public abstract double calcularArea();
}

class Circulo extends Figura {
private double radio;
public Circulo(String color, double radio) { super(color); this.radio = radio; }
@Override
public double calcularArea() { return Math.PI * radio * radio; }
}

class Rectangulo extends Figura {
private double base;
private double altura;
public Rectangulo(String color, double base, double altura) {
super(color); this.base = base; this.altura = altura;
}
@Override
public double calcularArea() { return base * altura; }
}

Una clase abstract no puede instanciarse. Puede contener atributos, constructor y métodos implementados. Un método abstracto declara una operación, pero deja su implementación a las clases hijas.

Referencia padre, objeto hijo​

Figura figura = new Circulo("azul", 2);

La variable tiene tipo Figura, pero el objeto real es un Circulo. Esta conversión hacia el tipo padre, llamada upcasting, es automática y segura.

Al ejecutar figura.calcularArea(), Java elige la implementación del objeto real. Este despacho dinámico depende del objeto al que apunta la referencia. La referencia solo ofrece las operaciones declaradas por Figura.

Distintos objetos, un mismo recorrido​

abstract class Figura {
private String color;
public Figura(String color) { this.color = color; }
public String getColor() { return color; }
public abstract double calcularArea();
}
class Circulo extends Figura {
private double radio;
public Circulo(String color, double radio) { super(color); this.radio = radio; }
@Override public double calcularArea() { return Math.PI * radio * radio; }
}
class Rectangulo extends Figura {
private double base; private double altura;
public Rectangulo(String color, double base, double altura) { super(color); this.base = base; this.altura = altura; }
@Override public double calcularArea() { return base * altura; }
}
public class FigurasDemo {
public static void main(String[] args) {
Figura[] figuras = { new Circulo("azul", 2), new Rectangulo("verde", 3, 4) };
for (Figura figura : figuras) {
System.out.println(figura.getColor() + ": " + figura.calcularArea());
}
}
}

El bucle no pregunta qué subtipo recibe. Cada objeto responde con su propia fórmula. Añadir otra figura no obliga a reescribir el recorrido.

¿Clase concreta o abstracta?​

Usa una clase concreta si tiene sentido crear objetos de ese tipo y su comportamiento está completo. Usa una abstracta cuando el tipo común aporta estado o implementación, pero necesita especializaciones para completar operaciones esenciales.

Errores habituales​

  • Intentar new Figura(...).
  • Dejar sin implementar un método abstracto en una hija concreta.
  • Confundir el tipo de la referencia con el tipo real del objeto.
  • Crear cadenas de herencia largas para conseguir polimorfismo.
  • Preguntar por el subtipo cuando basta con llamar al método común.

Practica: notificaciones​

Enunciado y criterios​

Crea una clase abstracta Notificacion con destinatario, un getter y el método abstracto mensaje(). Implementa email y SMS. Recorre ambas mediante un array del tipo padre, sin preguntar por el subtipo.

Solución​

abstract class Notificacion {
private String destinatario;
public Notificacion(String destinatario) { this.destinatario = destinatario; }
public String getDestinatario() { return destinatario; }
public abstract String mensaje();
}
class NotificacionEmail extends Notificacion {
public NotificacionEmail(String destinatario) { super(destinatario); }
@Override public String mensaje() { return "Email para " + getDestinatario(); }
}
class NotificacionSms extends Notificacion {
public NotificacionSms(String destinatario) { super(destinatario); }
@Override public String mensaje() { return "SMS para " + getDestinatario(); }
}
public class NotificacionesDemo {
public static void main(String[] args) {
Notificacion[] pendientes = {
new NotificacionEmail("ana@example.com"), new NotificacionSms("600123123")
};
for (Notificacion notificacion : pendientes) {
System.out.println(notificacion.mensaje());
}
}
}

La clase padre aporta estado compartido; las hijas completan la operación. A continuación veremos cómo expresar un contrato común cuando no necesitamos compartir estado ni constructor.