Al final de la lección anterior ocurrió algo que dejamos sin explicar. El método imprimirFicha(Material m) recibía indistintamente un Libro, una Revista o un Dvd, y cada uno respondía según lo que realmente era: el libro cobraba 0,25 € por día, el DVD 0,50 €, la revista 0,10 €. Y todo eso con una única línea de código de cálculo, escrita una sola vez en Material. Ese mecanismo se llama polimorfismo —del griego "muchas formas"— y es el pilar que justifica la existencia de la herencia. Sin él, una jerarquía de clases sería poco más que una forma de ahorrar líneas; con él, se convierte en la herramienta que permite añadir tipos nuevos sin modificar el código existente. Esta lección explica cómo funciona por dentro, cómo se usa bien, y por qué los if y switch sobre el tipo de un objeto —los que llenaban tu main en el módulo 2— son un síntoma de que falta polimorfismo.

Contenido

  1. Dos polimorfismos distintos
  2. Tipo declarado frente a tipo real
  3. El despacho dinámico paso a paso
  4. El ejemplo central: Material que calcula multas
  5. Upcasting: la conversión implícita
  6. Downcasting y ClassCastException
  7. instanceof y el patrón de Java 16
  8. Los if sobre el tipo son un olor de diseño
  9. Los miembros static no son polimórficos: hiding
  10. Campos y polimorfismo: tampoco
  11. Polimorfismo con arrays de la superclase
  12. Errores Comunes y Consejos
  13. Ejercicios

  1. Dos polimorfismos distintos

En Java conviven dos formas de polimorfismo que conviene no confundir:

Aspecto Polimorfismo en compilación Polimorfismo en ejecución
Otro nombre Estático, ad hoc Dinámico, de subtipos
Mecanismo Sobrecarga de métodos Sobrescritura de métodos
Quién decide El compilador La JVM, en tiempo de ejecución
Con qué información decide Los tipos declarados de los argumentos El tipo real del objeto
Necesita herencia No
Ejemplo calcularMulta(int) y calcularMulta(int, double) Dvd.getTarifaDiaria() sobre Material.getTarifaDiaria()

El primero ya lo dominas: lo estudiaste en 03-03 y no tiene misterio, el compilador mira los tipos escritos y elige. El segundo es el interesante y el que da nombre al pilar de la POO; cuando alguien dice "polimorfismo" a secas, casi siempre se refiere a este.

  1. Tipo declarado frente a tipo real

Toda referencia en Java tiene dos tipos que pueden no coincidir:

Material m = new Dvd("Curso de Spring", "DVD-0007", 240);
//       ^                 ^
//  tipo declarado     tipo real
Concepto Qué es Quién lo usa Cuándo se conoce
Tipo declarado (estático) El tipo escrito a la izquierda de la variable El compilador, para decidir qué llamadas son legales En compilación
Tipo real (dinámico) La clase del objeto que se creó con new La JVM, para decidir qué código ejecutar En ejecución

Y de ahí salen las dos reglas que gobiernan todo lo demás:

El tipo declarado decide QUÉ PUEDES LLAMAR. El tipo real decide QUÉ SE EJECUTA.

Compruébalo:

Material m = new Dvd("Curso de Spring", "DVD-0007", 240);

System.out.println(m.getTipo());          // "DVD"  <-- ejecuta la version de Dvd
System.out.println(m.getDiasPrestamo());  // 3      <-- ejecuta la version de Dvd

System.out.println(m.duracionMinutos);    // ERROR DE COMPILACION
error: cannot find symbol
  symbol:   variable duracionMinutos
  location: variable m of type Material

El objeto es un DVD y tiene su campo duracionMinutos, pero el compilador solo ve un Material y Material no declara ese campo. La restricción no es caprichosa: el compilador debe garantizar que la llamada sea válida para cualquier objeto que pudiera estar ahí, y en esa variable podría haber un Libro.

  1. El despacho dinámico paso a paso

El mecanismo por el que la JVM elige la implementación correcta se llama despacho dinámico (dynamic dispatch o late binding). Funciona así:

flowchart TD
    A["El compilador ve
    m.getTarifaDiaria()
    con m de tipo Material"] --> B{"¿Material declara
    ese metodo?"}
    B -- "no" --> E["Error de compilacion"]
    B -- "si" --> C["Compila la llamada
    y la deja sin resolver"]
    C --> D["En EJECUCION la JVM
    mira el tipo real del objeto"]
    D --> F["¿Dvd sobrescribe
    getTarifaDiaria?"]
    F -- "si" --> G["Ejecuta Dvd.getTarifaDiaria
    devuelve 0.50"]
    F -- "no" --> H["Sube por la jerarquia
    hasta encontrar la version heredada"]

En dos frases: el compilador verifica que el método existe en el tipo declarado, y la JVM busca la implementación empezando por la clase real del objeto y subiendo por la jerarquía hasta encontrarla. Por eso Libro, que no sobrescribe getTarifaDiaria(), acaba ejecutando la de Material.

Este mecanismo tiene un coste mínimo en rendimiento (la JVM usa una tabla de métodos y además optimiza agresivamente en caliente), y a cambio da una flexibilidad enorme. En el módulo 10-07 verás cómo el compilador JIT llega incluso a eliminar ese coste cuando detecta que en la práctica siempre se ejecuta la misma implementación.

  1. El ejemplo central: Material que calcula multas

Este es el ejemplo que resume el módulo entero. Fíjate en calcularMulta, escrito una sola vez en Material:

public double calcularMulta(int diasTranscurridos) {
    int retraso = Math.max(0, diasTranscurridos - getDiasPrestamo());
    return Math.min(retraso * getTarifaDiaria(), MULTA_MAXIMA);
}

Ese método no sabe —ni le importa— si el objeto es un libro, una revista o un DVD. Llama a getDiasPrestamo() y getTarifaDiaria(), y el despacho dinámico se encarga de que cada material aporte sus propios números.

Material m1 = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
Material m2 = new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral");
Material m3 = new Dvd("Curso de Spring", "DVD-0007", 240);

System.out.printf("%-12s %-22s %8s %10s %10s  %s%n",
                  "TIPO", "TITULO", "PLAZO", "TARIFA", "MULTA 12d", "GRAVEDAD");
mostrar(m1);
mostrar(m2);
mostrar(m3);
/** Un solo metodo sirve para todos los soportes, presentes y futuros. */
private static void mostrar(Material m) {
    System.out.printf("%-12s %-22s %6d d %9.2f %9.2f  %s%n",
                      m.getTipo(), m.titulo, m.getDiasPrestamo(), m.getTarifaDiaria(),
                      m.calcularMulta(12), m.clasificarGravedad(12));
}

Salida:

TIPO         TITULO                    PLAZO     TARIFA  MULTA 12d  GRAVEDAD
Libro        Java Efectivo               15 d      0,25       0,00  SIN RETRASO
Revista      Java Magazine                7 d      0,10       0,50  LEVE
DVD          Curso de Spring              3 d      0,50       4,50  GRAVE

Verifícalo a mano: con 12 días transcurridos, el libro sigue en plazo (15 días); la revista lleva 5 de retraso (12 − 7) y paga 5 × 0,10 = 0,50 €, retraso leve porque 5 ≤ 7; el DVD lleva 9 de retraso (12 − 3) y paga 9 × 0,50 = 4,50 €, grave porque 9 > 7.

Tres líneas de código de negocio y tres comportamientos distintos. Y la propiedad más valiosa: si mañana Nexus Software añade mapas técnicos o kits de robótica, mostrar seguirá funcionando sin tocarse.

  1. Upcasting: la conversión implícita

Asignar un objeto de una subclase a una referencia de la superclase se llama upcasting (conversión hacia arriba). Es implícito y siempre seguro, porque un Dvd siempre es un Material:

Dvd dvd = new Dvd("Curso de Spring", "DVD-0007", 240);
Material m = dvd;              // upcasting implicito, sin sintaxis especial

// tambien ocurre al pasar parametros...
mostrar(dvd);                  // el parametro es Material

// ...y al declarar directamente
Material otro = new Dvd("Curso de Docker", "DVD-0008", 180);

Qué cambia y qué no con el upcasting:

Cambia No cambia
Lo que el compilador te deja llamar (solo lo de Material) El objeto: sigue siendo exactamente el mismo Dvd
Qué implementación se ejecuta: la de Dvd
Los campos propios: siguen ahí, solo que inaccesibles por esa referencia

Es importante interiorizar que el upcasting no transforma nada: no crea un nuevo objeto ni recorta el original. Solo cambia las gafas con las que lo mira el compilador.

  1. Downcasting y ClassCastException

El downcasting es la operación inversa: tratar una referencia de la superclase como si fuera de la subclase. Requiere sintaxis explícita, porque no siempre es seguro:

Material m = new Dvd("Curso de Spring", "DVD-0007", 240);

Dvd dvd = (Dvd) m;                        // downcasting explicito
System.out.println(dvd.duracionMinutos);  // 240: ya se puede acceder

Si el objeto real no es de ese tipo, el programa falla en ejecución:

Material m = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
Dvd dvd = (Dvd) m;     // compila... y revienta al ejecutar
Exception in thread "main" java.lang.ClassCastException:
        class com.nexussoftware.bibliotech.dominio.Libro cannot be cast to
        class com.nexussoftware.bibliotech.dominio.Dvd
        at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:42)

El compilador acepta la conversión porque podría ser válida (un Material podría ser un Dvd), pero la comprobación real ocurre en ejecución. La ClassCastException se estudia como excepción en el módulo 6; aquí basta con saber que existe y cómo evitarla: comprobando el tipo antes con instanceof.

  1. instanceof y el patrón de Java 16

El operador instanceof responde a la pregunta "¿este objeto es de este tipo?":

Material m = obtenerMaterial();

if (m instanceof Dvd) {
    Dvd dvd = (Dvd) m;                     // downcasting ya seguro
    System.out.println("Duracion: " + dvd.duracionMinutos + " min");
}

Ese patrón —comprobar, castear, declarar variable— era tan repetitivo que Java 16 lo integró en el propio operador. Es el instanceof con patrón de tipo:

if (m instanceof Dvd dvd) {                // comprueba, convierte y declara
    System.out.println("Duracion: " + dvd.duracionMinutos + " min");
}

La variable dvd solo existe donde el compilador puede garantizar que la comprobación fue cierta, incluso en condiciones compuestas:

// Funciona: tras && el compilador ya sabe que dvd es valida
if (m instanceof Dvd dvd && dvd.duracionMinutos > 120) {
    System.out.println("DVD largo: " + dvd.titulo);
}

// Tambien funciona con negacion y salida anticipada
if (!(m instanceof Libro libro)) {
    return;
}
System.out.println(libro.autor);   // aqui libro esta garantizada

Dos detalles útiles:

  • null instanceof Cualquiera devuelve false, nunca lanza error. Eso convierte a instanceof en una guarda de nulidad implícita.
  • instanceof es cierto también para las superclases: dvd instanceof Material es true.

  1. Los if sobre el tipo son un olor de diseño

Ahora la parte importante de la lección. Compara estas dos formas de resolver el mismo problema.

Sin polimorfismo, tal como habrías tenido que escribirlo con lo que sabías en el módulo 2:

// ANTES: la logica de cada soporte, dispersa en un switch sobre el tipo
public static double calcularMulta(String tipoMaterial, int diasTranscurridos) {

    int    plazo;
    double tarifa;

    switch (tipoMaterial) {
        case "LIBRO" -> { plazo = 15; tarifa = 0.25; }
        case "REVISTA" -> { plazo =  7; tarifa = 0.10; }
        case "DVD" -> { plazo =  3; tarifa = 0.50; }
        default -> { plazo = 15; tarifa = 0.25; }
    }

    int retraso = Math.max(0, diasTranscurridos - plazo);
    return Math.min(retraso * tarifa, 20.0);
}

Con polimorfismo:

// DESPUES: cada soporte conoce sus reglas; el calculo es unico
public double calcularMulta(int diasTranscurridos) {
    int retraso = Math.max(0, diasTranscurridos - getDiasPrestamo());
    return Math.min(retraso * getTarifaDiaria(), MULTA_MAXIMA);
}

La comparación, punto por punto:

Criterio switch sobre el tipo Polimorfismo
Añadir un audiolibro Modificar el switch... Escribir una clase nueva
...y todos los demás Y el de listar, el de validar, el de imprimir... Nada más
Riesgo de olvidar un sitio Alto: los switch se dispersan por el código Nulo: el compilador reúne todo en la clase
Dónde está la regla del DVD Repartida por N métodos En Dvd.java, y solo ahí
Qué pasa con un tipo desconocido Cae en default, silenciosamente No hay caso desconocido

El criterio profesional, que conviene memorizar:

Si escribes un if o un switch que pregunta de qué tipo es un objeto para decidir qué hacer, casi siempre falta un método polimórfico.

Esto no significa que instanceof esté prohibido. Hay usos legítimos:

  • Implementar equals (lección 03-09).
  • Trabajar con APIs ajenas cuya jerarquía no puedes modificar.
  • Recuperar un dato específico de un subtipo para presentarlo, como el duracionMinutos de un DVD en una ficha detallada, cuando ese dato no tiene equivalente en los demás.

El caso ilegítimo es el que sustituye a un comportamiento que debería vivir en las clases:

// MAL: comportamiento de negocio decidido desde fuera
double tarifa;
if (m instanceof Libro)        tarifa = 0.25;
else if (m instanceof Revista) tarifa = 0.10;
else if (m instanceof Dvd)     tarifa = 0.50;
else                           tarifa = 0.25;

// BIEN
double tarifa = m.getTarifaDiaria();

  1. Los miembros static no son polimórficos: hiding

Aquí llega una trampa que hay que conocer para no caer en ella. Los métodos static no se sobrescriben: se ocultan (hiding), y se resuelven con el tipo declarado, no con el tipo real.

public class Material {
    public static String descripcionTipo() {
        return "Material generico";
    }
}

public class Dvd extends Material {
    public static String descripcionTipo() {     // OCULTA, no sobrescribe
        return "DVD de formacion";
    }
}
Material m = new Dvd("Curso de Spring", "DVD-0007", 240);

System.out.println(m.descripcionTipo());          // "Material generico"  (!)
System.out.println(Material.descripcionTipo());   // "Material generico"
System.out.println(Dvd.descripcionTipo());        // "DVD de formacion"

La primera línea es la sorprendente. El objeto es un Dvd, pero como descripcionTipo() es static, la JVM no hace despacho dinámico: usa el tipo declarado de m, que es Material. Es exactamente lo contrario de lo que hacen los métodos de instancia.

Contraste directo, en una tabla:

Método de instancia Método static
Redefinir en la subclase se llama Sobrescritura (override) Ocultación (hiding)
Se resuelve con Tipo real del objeto Tipo declarado de la referencia
Admite @Override No (error de compilación)
Es polimórfico No

Prueba de fuego: si añades @Override al método static de Dvd, el compilador falla con "method does not override or implement a method from a supertype". Otra razón más para escribir siempre la anotación.

Consejo práctico: nunca llames a un método static a través de una referencia (m.descripcionTipo()). Escribe siempre Material.descripcionTipo() o Dvd.descripcionTipo(), y la ambigüedad desaparece del código.

  1. Campos y polimorfismo: tampoco

La misma regla se aplica a los campos: los campos no son polimórficos, se resuelven con el tipo declarado.

public class Material {
    public String etiqueta = "MATERIAL";
}

public class Dvd extends Material {
    public String etiqueta = "DVD";     // OCULTA el campo de Material
}
Dvd      dvd = new Dvd("Curso de Spring", "DVD-0007", 240);
Material mat = dvd;                     // la MISMA caja de memoria

System.out.println(dvd.etiqueta);       // "DVD"
System.out.println(mat.etiqueta);       // "MATERIAL"   <-- mismo objeto

El objeto contiene los dos campos a la vez, y cuál ves depende de con qué gafas lo mires. Es una fuente de errores tan absurda que la solución es sencilla: no declares en una subclase un campo con el mismo nombre que uno de la superclase. Y si lo que quieres es un valor que varíe por tipo, usa un método sobrescribible, como getTipo() en BiblioTech.

  1. Polimorfismo con arrays de la superclase

El polimorfismo brilla cuando tratas muchos objetos de forma uniforme. Para eso hace falta poder guardarlos juntos, y de momento la única herramienta disponible es un array:

Nota provisional. Los arrays tienen tamaño fijo y son incómodos para un catálogo real: no se puede añadir ni eliminar sin crear otro array. En el módulo 5 aparecerán ArrayList, HashMap y el resto del framework de colecciones, que es la solución definitiva. Usa el array aquí como andamio para ver el polimorfismo en acción.

Material[] catalogo = {
    new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
    new Libro("Patrones de Diseño", "Erich Gamma", "978-0000000002", 1994),
    new Libro("Refactorización", "Martin Fowler", "978-0000000003", 2018),
    new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral"),
    new Dvd("Curso de Spring", "DVD-0007", 240)
};

System.out.printf("%-12s %-22s %8s %10s  %s%n",
                  "TIPO", "TITULO", "PLAZO", "MULTA 12d", "GRAVEDAD");

double recaudacionPrevista = 0.0;

for (Material m : catalogo) {                 // una sola linea de codigo...
    System.out.printf("%-12s %-22s %6d d %9.2f  %s%n",
                      m.getTipo(), m.titulo, m.getDiasPrestamo(),
                      m.calcularMulta(12), m.clasificarGravedad(12));
    recaudacionPrevista += m.calcularMulta(12);   // ...y cada objeto aplica lo suyo
}

System.out.printf("%nRecaudacion prevista a 12 dias: %.2f EUR%n", recaudacionPrevista);

Salida:

TIPO         TITULO                    PLAZO  MULTA 12d  GRAVEDAD
Libro        Java Efectivo               15 d       0,00  SIN RETRASO
Libro        Patrones de Diseño          15 d       0,00  SIN RETRASO
Libro        Refactorización             15 d       0,00  SIN RETRASO
Revista      Java Magazine                7 d       0,50  LEVE
DVD          Curso de Spring              3 d       4,50  GRAVE

Recaudacion prevista a 12 dias: 5,00 EUR

Un array de Material puede contener objetos de cualquier subclase, y el bucle los trata a todos por igual mientras cada uno hace lo suyo. Este es, en esencia, el motivo por el que existe la POO en el software grande.

Un detalle a tener en cuenta: al recorrer el array, el tipo declarado de m es Material, así que solo puedes llamar a lo que Material declare. Si necesitas el autor de los libros, harás falta instanceof con patrón:

for (Material m : catalogo) {
    if (m instanceof Libro libro) {
        System.out.println(libro.titulo + " es de " + libro.autor);
    }
}

Y este es exactamente uno de los usos legítimos del apartado 8: extraer un dato que solo existe en un subtipo, no decidir comportamiento de negocio.

Errores Comunes y Consejos

  • Creer que el upcasting "convierte" el objeto. No lo toca. Solo limita lo que el compilador te deja llamar. El objeto sigue siendo lo que era, y sus métodos sobrescritos siguen ejecutándose.
  • Hacer downcasting sin comprobar. (Dvd) m sin un instanceof delante es una ClassCastException esperando su momento. Usa siempre if (m instanceof Dvd dvd).
  • Poner @Override a un método static. No compila, y esa es su virtud: te avisa de que no estás sobrescribiendo, sino ocultando.
  • Llamar a métodos static a través de una instancia. m.descripcionTipo() engaña sobre qué se va a ejecutar. Usa el nombre de la clase.
  • Redefinir un campo en la subclase. Acabas con dos campos con el mismo nombre en el mismo objeto y resultados que dependen del tipo declarado. No lo hagas.
  • Cadenas de else if (x instanceof ...). Casi siempre significan que falta un método en la jerarquía. Antes de escribir la tercera rama, pregúntate qué método debería tener Material.
  • Consejo: cuando añadas una subclase, prueba a listar qué métodos sobrescribe. Si no sobrescribe ninguno, quizá no necesitabas una subclase, sino un campo.
  • Consejo: programa contra el tipo más general que te sirva. Declara Material m en lugar de Dvd d cuando no necesites lo específico del DVD: tu código valdrá para más casos. Este principio se lleva al extremo en el módulo 4 con las interfaces.
  • Consejo: usa la marca del margen del IDE. IntelliJ y Eclipse muestran un icono junto a los métodos sobrescritos y permiten saltar a la implementación real con un clic.

Ejercicios

Ejercicio 1: añadir un tipo sin tocar el código existente

Este ejercicio comprueba la propiedad más valiosa del polimorfismo. Nexus Software incorpora kits de robótica al catálogo: se prestan 5 días, con tarifa de 1,00 €/día (son caros), y tienen un campo numeroPiezas.

  1. Crea KitRobotica como subclase de Material.
  2. Añádelo al array catalogo del apartado 11.
  3. Ejecuta sin modificar ni una línea de Material, Libro, Revista, Dvd, mostrar ni el bucle de listado.
  4. Verifica manualmente la multa a 12 días transcurridos y la gravedad.

Después responde: ¿cuántos ficheros has tenido que tocar? ¿Cuántos habrías tocado con el switch del apartado 8?

Ejercicio 2: refactorizar un switch sobre el tipo

Un compañero ha escrito este método en BiblioTechApp. Refactorízalo eliminando por completo el switch, moviendo cada responsabilidad a la clase que le corresponde. Indica qué métodos nuevos necesita Material y cuáles sobrescribe cada subclase.

public static String generarAviso(Material m, int diasTranscurridos) {
    String canal;
    int    diasPreaviso;

    switch (m.getTipo()) {
        case "Libro" -> { canal = "correo";     diasPreaviso = 3; }
        case "Revista" -> { canal = "chat";       diasPreaviso = 1; }
        case "DVD" -> { canal = "telefono";   diasPreaviso = 1; }
        default -> { canal = "correo";     diasPreaviso = 3; }
    }

    return "Aviso por " + canal + " con " + diasPreaviso
           + " dias de preaviso. Multa actual: "
           + m.calcularMulta(diasTranscurridos) + " EUR";
}

Ejercicio 3: demostrar hiding frente a overriding

Escribe una clase de prueba que demuestre en la misma ejecución las tres reglas del apartado 9 y 10:

  1. Un método de instancia sobrescrito ejecuta la versión del tipo real.
  2. Un método static redefinido ejecuta la versión del tipo declarado.
  3. Un campo redefinido se lee según el tipo declarado, aunque el objeto sea el mismo.

Imprime en cada caso el tipo declarado, el tipo real (con getClass().getSimpleName()) y el valor obtenido, y explica por escrito por qué el resultado 1 difiere de los resultados 2 y 3.

Soluciones

Solución 1

package com.nexussoftware.bibliotech.dominio;

/** Kit de robotica para formacion practica. Material caro y escaso. */
public class KitRobotica extends Material {

    public static final int    DIAS_PRESTAMO_KIT = 5;
    public static final double TARIFA_DIARIA_KIT = 1.00;

    public final int numeroPiezas;

    public KitRobotica(String titulo, String referencia, int numeroPiezas) {
        super(titulo, referencia, true);
        this.numeroPiezas = Math.max(0, numeroPiezas);
    }

    @Override
    public int getDiasPrestamo() { return DIAS_PRESTAMO_KIT; }

    @Override
    public double getTarifaDiaria() { return TARIFA_DIARIA_KIT; }

    @Override
    public String getTipo() { return "Kit"; }

    @Override
    public String describir() {
        return super.describir() + " - " + numeroPiezas + " piezas";
    }
}

Solo hay que añadirlo al array:

Material[] catalogo = {
    new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
    new Libro("Patrones de Diseño", "Erich Gamma", "978-0000000002", 1994),
    new Libro("Refactorización", "Martin Fowler", "978-0000000003", 2018),
    new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral"),
    new Dvd("Curso de Spring", "DVD-0007", 240),
    new KitRobotica("Kit Arduino Avanzado", "KIT-0001", 128)     // unica linea nueva
};

Salida (fila nueva marcada):

TIPO         TITULO                    PLAZO  MULTA 12d  GRAVEDAD
Libro        Java Efectivo               15 d       0,00  SIN RETRASO
Libro        Patrones de Diseño          15 d       0,00  SIN RETRASO
Libro        Refactorización             15 d       0,00  SIN RETRASO
Revista      Java Magazine                7 d       0,50  LEVE
DVD          Curso de Spring              3 d       4,50  GRAVE
Kit          Kit Arduino Avanzado         5 d       7,00  LEVE

Recaudacion prevista a 12 dias: 12,00 EUR

Verificación manual, prestando atención a la frontera: 12 − 5 = 7 días de retraso; 7 × 1,00 = 7,00 €, por debajo del tope de 20 €. Y la gravedad es LEVE, no GRAVE, porque la condición es retraso <= UMBRAL_LEVE y UMBRAL_LEVE vale exactamente 7. Es justo el tipo de valor límite que la lección 02-05 te enseñó a comprobar: los umbrales se prueban siempre con los tres valores frontera (6, 7 y 8), porque un < en lugar de un <= cambiaría este resultado sin que ninguna otra fila de la tabla se enterase.

Respuesta a la pregunta. Has tocado dos ficheros: uno nuevo (KitRobotica.java) y una línea en BiblioTechApp para darlo de alta. Con el switch del apartado 8 habrías tenido que localizar y modificar todos los métodos que contuvieran un switch sobre el tipo —cálculo de multa, listado, avisos, validación—, con el riesgo de olvidar alguno y que el kit acabara tratado como un libro sin que nadie se enterara.

Solución 2

El switch decide dos cosas —el canal de aviso y los días de preaviso— que son propiedades de cada soporte. Su sitio son las clases.

En Material, dos métodos nuevos con el valor por defecto, y un tercero que compone el mensaje una sola vez:

/** @return canal preferente para avisar del vencimiento de este material. */
public String getCanalAviso() {
    return "correo";
}

/** @return dias de antelacion con que se avisa del vencimiento. */
public int getDiasPreaviso() {
    return 3;
}

/** @return texto del aviso de vencimiento para este material. */
public String generarAviso(int diasTranscurridos) {
    return String.format("Aviso por %s con %d dias de preaviso. Multa actual: %.2f EUR",
                         getCanalAviso(), getDiasPreaviso(),
                         calcularMulta(diasTranscurridos));
}

En Revista:

@Override
public String getCanalAviso() { return "chat"; }

@Override
public int getDiasPreaviso() { return 1; }

En Dvd:

@Override
public String getCanalAviso() { return "telefono"; }

@Override
public int getDiasPreaviso() { return 1; }

Libro no sobrescribe nada: hereda "correo" y 3 días, que eran justamente el caso default del switch original. Y BiblioTechApp se queda sin el método:

System.out.println(m.generarAviso(12));

Resultados:

Material Antes (switch) Después (polimorfismo)
Libro correo, 3 días correo, 3 días (heredado)
Revista chat, 1 día chat, 1 día (sobrescrito)
Dvd teléfono, 1 día teléfono, 1 día (sobrescrito)
KitRobotica caía en default sin que nadie lo decidiera hereda el defecto explícitamente, o lo sobrescribe

Tres ganancias concretas: el default silencioso desaparece, cada soporte reúne todas sus reglas en su fichero, y añadir un tipo nuevo ya no obliga a recorrer el código buscando switch olvidados.

Solución 3

package com.nexussoftware.bibliotech;

class Base {
    public String campo = "campo de Base";

    public String metodoInstancia() { return "instancia de Base"; }
    public static String metodoStatic() { return "static de Base"; }
}

class Derivada extends Base {
    public String campo = "campo de Derivada";          // OCULTA el de Base

    @Override
    public String metodoInstancia() { return "instancia de Derivada"; }

    public static String metodoStatic() { return "static de Derivada"; }  // OCULTA
}

public class DemoPolimorfismo {

    public static void main(String[] args) {

        Derivada d = new Derivada();
        Base     b = d;                   // upcasting: MISMO objeto, otras gafas

        System.out.println("Tipo declarado de b: Base");
        System.out.println("Tipo real de b:      " + b.getClass().getSimpleName());
        System.out.println("b == d:              " + (b == d));
        System.out.println();

        // 1. Metodo de instancia: gana el TIPO REAL
        System.out.println("1) b.metodoInstancia() -> " + b.metodoInstancia());
        System.out.println("   d.metodoInstancia() -> " + d.metodoInstancia());

        // 2. Metodo static: gana el TIPO DECLARADO
        System.out.println("2) b.metodoStatic()    -> " + b.metodoStatic());
        System.out.println("   d.metodoStatic()    -> " + d.metodoStatic());

        // 3. Campo: gana el TIPO DECLARADO
        System.out.println("3) b.campo             -> " + b.campo);
        System.out.println("   d.campo             -> " + d.campo);
    }
}

Salida:

Tipo declarado de b: Base
Tipo real de b:      Derivada
b == d:              true

1) b.metodoInstancia() -> instancia de Derivada
   d.metodoInstancia() -> instancia de Derivada
2) b.metodoStatic()    -> static de Base
   d.metodoStatic()    -> static de Derivada
3) b.campo             -> campo de Base
   d.campo             -> campo de Derivada

Explicación. La línea b == d da true: hay un solo objeto y dos referencias, así que cualquier diferencia entre las tres salidas viene del tipo declarado, no del objeto.

El caso 1 usa despacho dinámico: los métodos de instancia se resuelven en ejecución mirando la clase real, que es Derivada en ambos casos. Es el único de los tres que es polimórfico.

Los casos 2 y 3 se resuelven en compilación. El compilador sustituye b.metodoStatic() por Base.metodoStatic() y b.campo por el campo declarado en Base, porque ese es el tipo con el que se declaró la variable. La JVM ni siquiera llega a preguntarse qué objeto hay detrás. Por eso el mismo objeto exhibe dos valores distintos para campo: el objeto contiene ambos campos, y cada referencia lee el suyo.

La moraleja práctica de los tres casos juntos: el polimorfismo es cosa exclusiva de los métodos de instancia. Cualquier otro mecanismo —static, campos— se resuelve por el tipo escrito y produce sorpresas si intentas usarlo como si fuera polimórfico.

Conclusión

Has llegado al núcleo de la orientación a objetos. Distingues los dos polimorfismos —la sobrecarga, que resuelve el compilador, y la sobrescritura, que resuelve la JVM— y comprendes la regla que los gobierna: el tipo declarado decide qué puedes llamar, el tipo real decide qué se ejecuta. Has seguido el despacho dinámico paso a paso y has visto su consecuencia práctica en el ejemplo central del módulo: un único calcularMulta en Material que cobra 0,25 € a un libro, 0,10 € a una revista y 0,50 € a un DVD, sin un solo if sobre el tipo. Dominas el upcasting implícito y seguro, el downcasting explícito y arriesgado con su ClassCastException, y la forma moderna de protegerte con instanceof y patrón de tipo (if (m instanceof Dvd dvd)), que en una línea comprueba, convierte y declara. Sabes reconocer los if/switch sobre el tipo como un olor de diseño y refactorizarlos moviendo cada regla a la clase que le corresponde. Y conoces las dos excepciones que hay que tener siempre presentes: los métodos static no se sobrescriben, se ocultan, y los campos tampoco son polimórficos; ambos se resuelven por el tipo declarado.

Sobre todo, has comprobado en la práctica la propiedad que justifica todo el andamiaje: añadir un kit de robótica al catálogo ha costado una clase nueva y una línea, sin tocar nada de lo que ya funcionaba.

Queda una debilidad grave en el diseño actual, y la habrás notado: todos los campos siguen siendo public. Cualquiera puede escribir libro.disponible = true saltándose los avisos de prestar(), y nada impide asignar a un préstamo una multa inventada. En la lección siguiente, Encapsulamiento, cerrarás esa puerta: harás privados los campos de Libro, Empleado y Prestamo, aprenderás la tabla completa de modificadores de acceso, verás por qué poner un setter a cada campo destruye precisamente lo que se pretendía proteger, y descubrirás la inmutabilidad y las copias defensivas como herramientas para que el estado de tus objetos sea, de verdad, suyo.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados