# Día 3: Programación Orientada a Objetos de Verdad: Herencia, Interfaces y Polimorfismo

> Clases abstractas y jerarquías sealed, interfaces con métodos default, sobrescritura y enlace dinámico, static y final, y enums con comportamiento

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 3 de 30
- Fase 1: Java Vanilla — El Núcleo del Lenguaje (17 & 21)
- Dedicación: 4h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/poo-herencia-interfaces-polimorfismo/

## Al terminar este día sabrás

- Diseñar una jerarquía con clases abstractas o `sealed` y justificar cuándo preferir composición.
- Usar interfaces con métodos `default`, `static` y `private` sin convertirlas en clases.
- Distinguir sobrecarga de sobrescritura y predecir qué método se ejecuta.
- Explicar el orden de inicialización y modelar estados con enums con comportamiento.


## Conceptos

### 1. Herencia, Clases Abstractas y Jerarquías Sealed

Una subclase hereda campos y métodos de su superclase con `extends` (solo de una). Una clase `abstract` no se puede instanciar y puede declarar métodos sin cuerpo que las subclases deben implementar. Con `sealed ... permits` decides exactamente qué clases pueden heredar.

**Cómo funciona por dentro.**

Con `extends`, la subclase hereda los miembros no privados de la superclase y puede añadir o redefinir comportamiento. El constructor de la subclase llama siempre primero a un constructor de la madre (`super(...)`), de forma explícita o implícita. Una clase `abstract` sirve de plantilla: no se instancia, puede tener estado y métodos concretos, y obliga a las hijas a implementar sus métodos `abstract`.

El riesgo de la herencia es el acoplamiento: la hija depende de cómo está implementada la madre, no solo de lo que promete. Si la madre cambia un método del que depende la hija, la hija puede romperse sin que nadie la toque. Por eso la regla práctica es heredar solo cuando la relación es «es un» y la hija puede sustituir a la madre en cualquier sitio (principio de Liskov); en el resto de casos, composición.

Desde Java 17, `sealed` permite cerrar la jerarquía: `sealed class Pago permits Tarjeta, Bizum` dice que no hay más subclases posibles. Cada subclase permitida debe declararse `final`, `sealed` o `non-sealed`. A cambio, el compilador conoce todos los casos y un `switch` con patrones puede ser exhaustivo sin `default`.

*Jerarquía sealed con clase abstracta, y composición*

```java
public abstract sealed class Pago permits PagoTarjeta, PagoBizum {
    protected final BigDecimal importe;

    protected Pago(BigDecimal importe) {
        if (importe.signum() <= 0) throw new IllegalArgumentException("importe <= 0");
        this.importe = importe;
    }

    public abstract BigDecimal comision();            // cada tipo la calcula a su manera

    public BigDecimal total() {                        // comportamiento común heredado
        return importe.add(comision());
    }
}

public final class PagoTarjeta extends Pago {
    public PagoTarjeta(BigDecimal importe) { super(importe); }
    @Override public BigDecimal comision() { return importe.multiply(new BigDecimal("0.015")); }
}

public final class PagoBizum extends Pago {
    public PagoBizum(BigDecimal importe) { super(importe); }
    @Override public BigDecimal comision() { return BigDecimal.ZERO; }
}

// Composición en lugar de herencia: el servicio TIENE un notificador
public class ServicioCobros {
    private final Notificador notificador;
    public ServicioCobros(Notificador notificador) { this.notificador = notificador; }
}
```

- **En una línea:** `public abstract sealed class Pago permits PagoTarjeta, PagoBizum { abstract BigDecimal comision(); }`
- **Por qué importa:** La herencia reutiliza comportamiento, pero acopla mucho: la subclase depende de los detalles internos de la madre. Saber cuándo usarla y cuándo preferir composición es una pregunta clásica de diseño.
- **Error típico:** Heredar solo para reutilizar código (`class Pila extends ArrayList`), exponiendo métodos que rompen las reglas de la subclase.
- **Java 17 frente a 21:** Las clases e interfaces `sealed` son estándar desde Java 17. En Java 21, `switch` con patrones las aprovecha para comprobar que tratas todos los casos.
- **Se conecta con:** Composición frente a herencia, Principio de sustitución de Liskov, Sealed + switch

**Compruébalo:** ¿Compila `final class PagoCripto extends Pago` en otro fichero si no aparece en `permits`?

No. Solo las clases listadas en `permits` pueden heredar de una clase `sealed`; el compilador rechaza cualquier otra.

### 2. Interfaces: Contratos con Métodos default, static y private

Una interfaz define qué sabe hacer un tipo sin decir cómo, y una clase puede implementar varias. Desde Java 8 admiten métodos `default` (con cuerpo, heredables) y `static`; desde Java 9, métodos `private` para compartir código entre ellos.

**Cómo funciona por dentro.**

Una interfaz es un contrato: declara métodos que las clases que la implementan deben ofrecer. Al contrario que con las clases, una clase puede implementar varias interfaces, y eso permite describir capacidades independientes (`Comparable`, `AutoCloseable`, `Notificador`).

Los métodos `default` (Java 8) añaden comportamiento con cuerpo que las implementaciones heredan y pueden sobrescribir. Nacieron para ampliar interfaces existentes, como `Collection.stream()`, sin romper todas las clases que ya las implementaban. Los métodos `static` son utilidades ligadas a la interfaz (`List.of`, `Comparator.comparing`) y los `private` (Java 9) comparten código entre métodos `default`. Lo que una interfaz no puede tener son campos de instancia: solo constantes `static final`.

Regla práctica: interfaz cuando describes qué sabe hacer algo y quieres poder tener varias implementaciones; clase abstracta cuando además compartes estado o el esqueleto de un algoritmo. En Spring, depender de interfaces es lo que permite cambiar la implementación real por un doble en los tests.

*Interfaz con métodos default, static y private*

```java
public interface Notificador {
    void enviar(String destino, String mensaje);

    default void enviarATodos(List<String> destinos, String mensaje) {
        destinos.stream()
            .filter(Notificador::valido)
            .forEach(d -> enviar(d, formatear(mensaje)));
    }

    static Notificador porConsola() {                 // fábrica asociada al contrato
        return (destino, mensaje) -> System.out.println(destino + " <- " + mensaje);
    }

    private String formatear(String mensaje) {         // compartido entre métodos default
        return "[JavaMastery] " + mensaje.strip();
    }

    private static boolean valido(String destino) {
        return destino != null && !destino.isBlank();
    }
}

class NotificadorEmail implements Notificador {
    @Override
    public void enviar(String destino, String mensaje) { /* SMTP */ }
}
```

- **En una línea:** `interface Notificador { void enviar(String msg); default void enviarTodos(List<String> msgs) { msgs.forEach(this::enviar); } }`
- **Por qué importa:** Programar contra interfaces permite cambiar implementaciones (un repositorio real por uno falso en los tests) y es la base de la inyección de dependencias de Spring.
- **Error típico:** Usar métodos `default` para meter lógica de negocio con estado: las interfaces no tienen campos de instancia.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Inyección de dependencias, Mockito, Interfaces funcionales

### 3. Sobrecarga frente a Sobrescritura y Enlace Dinámico

Sobrecarga: varios métodos con el mismo nombre y distintos parámetros; el compilador elige cuál según los tipos declarados. Sobrescritura: una subclase redefine un método heredado con la misma firma; la JVM elige en tiempo de ejecución según el tipo real del objeto.

**Cómo funciona por dentro.**

La sobrecarga la resuelve el compilador. Cuando escribes `imprimir(x)`, busca entre los métodos `imprimir` el más específico para el tipo declarado de `x`, teniendo en cuenta conversiones, autoboxing y varargs, por ese orden de preferencia. La decisión queda fijada en el bytecode: es enlace estático.

La sobrescritura la resuelve la JVM al ejecutar. Si `Animal a = new Perro()` y llamas a `a.sonido()`, el compilador solo comprueba que `Animal` tiene ese método; en ejecución la JVM mira la clase real del objeto y ejecuta `Perro.sonido()`. Eso es enlace dinámico, y es lo que hace funcionar el polimorfismo. Los métodos `static`, `private` y `final` no participan: se resuelven siempre de forma estática.

Para sobrescribir, la firma debe coincidir (el tipo de retorno puede ser un subtipo: retorno covariante), no puedes reducir la visibilidad ni declarar excepciones comprobadas más amplias. `@Override` hace que el compilador lo compruebe todo: sin la anotación, un error en la firma crea un método nuevo en silencio.

*Qué método se ejecuta en cada caso*

```java
class Animal {
    String sonido() { return "..."; }
    static String tipo() { return "animal"; }
}

class Perro extends Animal {
    @Override String sonido() { return "guau"; }      // sobrescritura: decide la JVM
    static String tipo() { return "perro"; }           // ocultación, NO sobrescritura
}

class Impresora {
    void imprimir(Object o) { System.out.println("Object"); }
    void imprimir(String s) { System.out.println("String"); }   // sobrecarga: decide el compilador
}

Animal a = new Perro();
System.out.println(a.sonido());     // guau   (cuenta el tipo real del objeto)
System.out.println(Animal.tipo());  // animal (los static no son polimórficos)

Object texto = "hola";
new Impresora().imprimir(texto);    // Object: cuenta el tipo DECLARADO, no el real

// Trampa clásica: esto sobrecarga equals, no lo sobrescribe
// public boolean equals(Tarea otra) { ... }    -> con @Override no compilaría
```

- **En una línea:** `Animal a = new Perro(); a.sonido();` ejecuta `Perro.sonido()` aunque la variable sea de tipo `Animal`.
- **Por qué importa:** Es la base del polimorfismo: el mismo `pago.comision()` ejecuta código distinto según el objeto. Confundir ambos mecanismos produce bugs que no avisan.
- **Error típico:** Creer que sobrescribes `equals(Tarea otra)` cuando en realidad lo estás sobrecargando: la firma correcta es `equals(Object o)`. `@Override` lo detecta al compilar.
- **Java 17 frente a 21:** Idéntico en Java 17 y 21.
- **Se conecta con:** Polimorfismo, @Override, Métodos static (ocultación)

**Compruébalo:** ¿Qué imprime `new Impresora().imprimir(null)`?

`String`. Las dos versiones aceptan `null` y el compilador elige la más específica: `String` es más específica que `Object`.

### 4. static, final y el Orden de Inicialización

`static` hace que un campo o método pertenezca a la clase y no a cada objeto. `final` impide reasignar una variable, sobrescribir un método o heredar de una clase. Al crear un objeto se ejecutan en orden: inicializadores estáticos (una vez por clase), campos e inicializadores de instancia y, al final, el cuerpo del constructor.

**Cómo funciona por dentro.**

Un miembro `static` pertenece a la clase: existe una sola copia, compartida por todos los objetos y todos los hilos, y se usa sin crear instancias. Es adecuado para constantes (`static final`), métodos de utilidad sin estado y fábricas. Un campo `static` mutable, en cambio, es estado global: cualquier parte del programa puede cambiarlo, los tests se contaminan entre sí y aparecen condiciones de carrera.

`final` tiene tres usos. En una variable o un campo, impide reasignarlo (el objeto al que apunta puede seguir cambiando). En un método, impide sobrescribirlo. En una clase, impide heredar de ella, como hace `String`. Los campos `final` tienen además una garantía del modelo de memoria: si el objeto se construye correctamente, otros hilos los ven inicializados.

El orden de inicialización es: al cargar la clase, los campos e inicializadores `static` en orden de aparición, una sola vez. Al crear cada objeto, primero se ejecuta el constructor de la madre; después, los inicializadores de campos y los bloques `{ }` de instancia en orden de aparición; y por último el resto del constructor. Llamar a un método sobrescribible desde un constructor es peligroso: la versión de la hija se ejecuta antes de que la hija haya inicializado sus campos.

*Orden de inicialización observable*

```java
class Base {
    static { System.out.println("1. static de Base"); }
    { System.out.println("3. instancia de Base"); }

    Base() {
        System.out.println("4. constructor de Base");
        describir();                                 // peligro: ejecuta la versión de la hija
    }

    void describir() { }
}

class Hija extends Base {
    static { System.out.println("2. static de Hija"); }
    private String nombre = "hija";
    { System.out.println("5. instancia de Hija"); }

    Hija() { System.out.println("6. constructor de Hija"); }

    @Override
    void describir() {
        System.out.println("   describir(): nombre = " + nombre);   // aún no inicializado
    }
}

new Hija();
// 1. static de Base
// 2. static de Hija
// 3. instancia de Base
// 4. constructor de Base
//    describir(): nombre = null
// 5. instancia de Hija
// 6. constructor de Hija
```

- **En una línea:** `private static final Logger log = LoggerFactory.getLogger(Servicio.class);`
- **Por qué importa:** Los campos `static` mutables son estado global compartido entre hilos y tests. Los campos `final` hacen los objetos más seguros y fáciles de razonar.
- **Error típico:** Guardar datos de una petición en un campo `static` (o en un bean singleton): se mezclan entre usuarios.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Inmutabilidad, Bloques de inicialización, Beans singleton

### 5. Enums con Estado y Comportamiento

Un `enum` es una clase con un conjunto fijo de instancias. Puede tener campos, constructor, métodos e incluso un cuerpo distinto por constante. `EnumMap` y `EnumSet` son colecciones optimizadas para claves enum.

**Cómo funciona por dentro.**

Cada constante de un `enum` es una instancia única de su clase, creada al cargarla. Como es una clase, puede tener campos `final`, un constructor (siempre privado), métodos y métodos abstractos que cada constante implementa con su propio cuerpo. Los enums implementan `equals`, `hashCode`, `compareTo` y la serialización de forma segura, y se pueden comparar con `==`.

`values()` devuelve todas las constantes y `valueOf("PENDIENTE")` convierte un texto en constante (lanza `IllegalArgumentException` si no existe). `EnumMap` y `EnumSet` son implementaciones internas con arrays y bits, mucho más rápidas y compactas que un `HashMap` o un `HashSet` con claves enum.

En JPA, `@Enumerated(EnumType.STRING)` guarda el nombre de la constante. El valor por defecto, `ORDINAL`, guarda su posición, y añadir o reordenar constantes corrompe en silencio los datos existentes.

*Enum con datos, comportamiento y EnumMap*

```java
public enum Prioridad {
    BAJA(72) {
        @Override String icono() { return "·"; }
    },
    MEDIA(24) {
        @Override String icono() { return "!"; }
    },
    ALTA(4) {
        @Override String icono() { return "!!!"; }
    };

    private final int horasLimite;

    Prioridad(int horasLimite) { this.horasLimite = horasLimite; }   // constructor privado

    abstract String icono();                                          // cada constante lo implementa

    public boolean vencida(Instant creada, Clock reloj) {
        return Duration.between(creada, reloj.instant()).toHours() > horasLimite;
    }
}

Map<Prioridad, Long> porPrioridad = tareas.stream()
    .collect(Collectors.groupingBy(Tarea::prioridad,
             () -> new EnumMap<>(Prioridad.class),
             Collectors.counting()));

// En la entidad JPA:
// @Enumerated(EnumType.STRING) private Prioridad prioridad;
```

- **En una línea:** `enum Estado { PENDIENTE, EN_CURSO, HECHA; boolean esFinal() { return this == HECHA; } }`
- **Por qué importa:** Sustituye a las constantes `int` o `String` sueltas: el compilador impide valores inválidos y `switch` puede comprobar que tratas todos los casos.
- **Error típico:** Persistir un enum en base de datos por su posición (`ordinal()`): si reordenas las constantes, los datos cambian de significado. Usa `@Enumerated(EnumType.STRING)`.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** switch exhaustivo, EnumMap, Patrón Strategy


## Sesión de 90 minutos

### Bloque 1: Herencia y Clases Abstractas (0 - 25 min)

- **Qué estudiar:** `extends`, `abstract`, `super` y `sealed ... permits`.
- **Pregunta a responder:** ¿Qué ventaja tiene una jerarquía `sealed` frente a una abierta?
- **Entregable:** Jerarquía `sealed` de tipos de pago con una clase abstracta y dos subclases.

### Bloque 2: Interfaces y Polimorfismo (25 - 50 min)

- **Qué estudiar:** Métodos `default`, `static` y `private` en interfaces; sobrescritura con `@Override`.
- **Pregunta a responder:** ¿Cuándo elegir clase abstracta y cuándo interfaz?
- **Entregable:** Interfaz `Notificador` con dos implementaciones y un test que use ambas a través de la interfaz.

### Bloque 3: static, final y Enums (50 - 80 min)

- **Qué estudiar:** Orden de inicialización, campos `static final` y enums con métodos.
- **Pregunta a responder:** ¿Por qué no se debe guardar `ordinal()` en base de datos?
- **Entregable:** Enum `Estado` con comportamiento y un `EnumMap` que cuente tareas por estado.

### Cierre de Consolidación (80 - 90 min)

- **Qué estudiar:** Repaso de la rúbrica del tutor.
- **Pregunta a responder:** Explicar en voz alta la diferencia entre sobrecarga y sobrescritura con un ejemplo.
- **Entregable:** Nota con tres situaciones en las que usarías composición en vez de herencia.


## Explícalo desde cero

### Por qué se dice «favorece la composición sobre la herencia»

- **Qué entender:** Heredar une la subclase a los detalles internos de la madre: si la madre cambia, la hija puede romperse sin tocarla. Componer (tener un objeto como campo y delegar en él) solo te une a su interfaz pública.
- **Qué suele explicarse mal:** Decir que la herencia es mala. Es correcta cuando la relación es de verdad «es un» y la subclase puede sustituir a la madre en cualquier sitio (principio de Liskov).
- **Ejemplo sencillo:** Un coche no «es un» motor: tiene un motor. Si mañana pasas a uno eléctrico, cambias la pieza, no la familia del coche.
- **Señal de alerta:** Si heredas de una clase solo para reutilizar uno o dos de sus métodos.


## Preguntas

### Nivel 1 (Básico): ¿Puede una clase implementar dos interfaces que tienen el mismo método `default`? ¿Qué pasa?

Solo compila si la clase sobrescribe ese método y resuelve el conflicto; dentro puede llamar a una de las versiones con `InterfazA.super.metodo()`. Si no lo sobrescribe, el compilador da error por ambigüedad.

**Respuesta razonada:** Solo si la clase sobrescribe el método. Java no puede elegir entre dos implementaciones `default` con la misma firma, así que la clase que las hereda debe declarar su propia versión. Dentro puede delegar en una de ellas con `A.super.metodo()` o combinar las dos.

Si uno de los dos métodos viniera de una clase madre en lugar de una interfaz, no habría conflicto: el método de la clase gana siempre sobre el `default` de una interfaz («las clases ganan»).

### Nivel 2 (Intermedio): Si un método `static` está en la clase padre y en la hija con la misma firma, ¿hay polimorfismo al llamarlo desde una variable del tipo padre?

No. Los métodos `static` no se sobrescriben, se ocultan: se elige al compilar según el tipo declarado de la variable, no según el objeto real. El polimorfismo solo funciona con métodos de instancia.

**Respuesta razonada:** No. Un método `static` con la misma firma en la hija oculta al de la madre, pero no lo sobrescribe. La llamada se resuelve al compilar según el tipo declarado: `Animal a = new Perro(); a.tipo()` ejecuta `Animal.tipo()`. Por eso los IDE avisan cuando se llama a un método estático a través de una instancia: parece polimórfico y no lo es.

El polimorfismo solo existe para métodos de instancia que no sean `private` ni `final`, que la JVM resuelve en ejecución según la clase real del objeto.

### Nivel 3 (Entrevista Senior): ¿Por qué `class Cuadrado extends Rectangulo` es el ejemplo clásico de violación del principio de Liskov?

Porque un `Rectangulo` permite cambiar ancho y alto por separado, y un cuadrado no. Si el código que usa `Rectangulo` hace `setAncho(5); setAlto(4)` y espera área 20, con un `Cuadrado` obtiene otra cosa. La subclase no puede sustituir a la madre sin romper sus expectativas.

**Respuesta razonada:** El principio de sustitución de Liskov dice que donde se usa la madre debe poder usarse cualquier hija sin que el programa se comporte mal. Un `Rectangulo` promete que ancho y alto se cambian por separado. Un `Cuadrado` no puede cumplirlo: o ignora uno de los setters o cambia los dos, y cualquier código que confíe en esa promesa falla.

En matemáticas un cuadrado es un rectángulo, pero en programación lo que importa es el comportamiento, no la taxonomía. Soluciones: hacer las figuras inmutables (sin setters, cada cambio crea una figura nueva) o no relacionarlas por herencia y que ambas implementen una interfaz `Figura` con `area()`.


## Reto del tutor

**Reto:** Un compañero propone crear `BaseService<T>` con `save`, `findAll` y `delete`, y que todos los servicios de la aplicación hereden de ella para no repetir código. Evalúa la propuesta.

- **Lo que está bien:** Detecta código repetido y busca reutilizarlo.
- **Respuesta superficial:** Decir simplemente que «la herencia es mala» o que «es mejor usar interfaces».
- **Lo que falta de fondo:** No ver que una clase base genérica obliga a todos los servicios a exponer operaciones que quizá no deben tener (no todos permiten borrar), los acopla a un único padre y mezcla lógica de negocio con acceso a datos.
- **Respuesta senior:** Los servicios representan casos de uso de negocio, no CRUD genérico, y lo genérico ya existe en Spring Data (`JpaRepository`). Si hay comportamiento común, mejor extraerlo a componentes que se inyectan (composición) o a interfaces pequeñas por capacidad. Herencia solo si todos los servicios «son un» mismo tipo y pueden sustituirse entre sí.


## Katas

### Kata 3.1: Jerarquía sealed de pagos

Crea `sealed interface Pago permits Tarjeta, Bizum, Transferencia` con records que la implementen y un método `comision(Pago p)` con `switch` sin `default`. Añade un nuevo tipo de pago y comprueba que el compilador te obliga a tratarlo.

### Kata 3.2: Enum con comportamiento

Modela `enum Prioridad { BAJA, MEDIA, ALTA }` con un campo `horasLimite` y un método `vencida(LocalDateTime creada, Clock reloj)`. Usa un `EnumMap<Prioridad, Long>` para contar tareas por prioridad y cúbrelo con tests.


## Reglas de oro

- Usa `@Override` siempre que sobrescribas: convierte un error silencioso en uno de compilación.
- Prefiere composición; hereda solo si la subclase puede sustituir a la madre en cualquier sitio.
- Nada de estado mutable en campos `static`.
- Guarda los enums por nombre (`EnumType.STRING`), nunca por posición.


## Java 17 frente a Java 21

| Característica | Java 17 | Java 21 |
| --- | --- | --- |
| Clases e interfaces sealed | Estables (JEP 409) | Estables y aprovechadas por switch con patrones |
| switch exhaustivo | Sobre enums en switch expressions | También sobre jerarquías sealed y records |


## Proyecto guiado «Pedidos»: Estados del pedido y medios de pago

Modelar el ciclo de vida del pedido y los medios de pago con tipos que el compilador pueda comprobar.

**Tareas:**

- Crea `enum EstadoPedido { BORRADOR, CONFIRMADO, PAGADO, ENVIADO, CANCELADO }` con un método `puedePasarA(EstadoPedido destino)`.
- Crea la clase `Pedido` (id, cliente, líneas, estado) con métodos `confirmar()`, `pagar()` y `cancelar()` que respetan las transiciones.
- Modela `sealed interface MetodoPago permits Tarjeta, Bizum, Transferencia` con records y una comisión distinta para cada uno.

**Está listo cuando:**

- Un pedido enviado no se puede cancelar ni un borrador pagar: lo impiden los métodos, no quien los llama.
- Añadir un nuevo medio de pago obliga a tocar el cálculo de comisiones, porque el `switch` deja de compilar.


## Fuentes oficiales

- [Tutorial de Java: interfaces y herencia](https://docs.oracle.com/javase/tutorial/java/IandI/index.html)
- [JEP 409: Sealed Classes](https://openjdk.org/jeps/409)
- [Java 21: guía de clases selladas](https://docs.oracle.com/en/java/javase/21/language/sealed-classes-and-interfaces.html)

