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
- Dos polimorfismos distintos
- Tipo declarado frente a tipo real
- El despacho dinámico paso a paso
- El ejemplo central:
Materialque calcula multas - Upcasting: la conversión implícita
- Downcasting y
ClassCastException instanceofy el patrón de Java 16- Los
ifsobre el tipo son un olor de diseño - Los miembros
staticno son polimórficos: hiding - Campos y polimorfismo: tampoco
- Polimorfismo con arrays de la superclase
- Errores Comunes y Consejos
- Ejercicios
- 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 | Sí |
| 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.
- Tipo declarado frente a tipo real
Toda referencia en Java tiene dos tipos que pueden no coincidir:
| 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 COMPILACIONEl 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.
- 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.
- El ejemplo central:
Material que calcula multas
Material que calcula multasEste 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.
- 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.
- Downcasting y
ClassCastException
ClassCastExceptionEl 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 accederSi 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 ejecutarException 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.
instanceof y el patrón de Java 16
instanceof y el patrón de Java 16El 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 garantizadaDos detalles útiles:
null instanceof Cualquieradevuelvefalse, nunca lanza error. Eso convierte ainstanceofen una guarda de nulidad implícita.instanceofes cierto también para las superclases:dvd instanceof Materialestrue.
- Los
if sobre el tipo son un olor de diseño
if sobre el tipo son un olor de diseñoAhora 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
ifo unswitchque 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
duracionMinutosde 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();
- Los miembros
static no son polimórficos: hiding
static no son polimórficos: hidingAquí 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 |
Sí | No (error de compilación) |
| Es polimórfico | Sí | 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.
- 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 objetoEl 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.
- 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,HashMapy 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) msin uninstanceofdelante es unaClassCastExceptionesperando su momento. Usa siempreif (m instanceof Dvd dvd). - Poner
@Overridea un métodostatic. No compila, y esa es su virtud: te avisa de que no estás sobrescribiendo, sino ocultando. - Llamar a métodos
statica 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 tenerMaterial. - 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 men lugar deDvd dcuando 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.
- Crea
KitRoboticacomo subclase deMaterial. - Añádelo al array
catalogodel apartado 11. - Ejecuta sin modificar ni una línea de
Material,Libro,Revista,Dvd,mostrarni el bucle de listado. - 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:
- Un método de instancia sobrescrito ejecuta la versión del tipo real.
- Un método
staticredefinido ejecuta la versión del tipo declarado. - 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:
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
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
