Al final de la lliçó anterior va passar una cosa que vam deixar sense explicar. El mètode imprimirFitxa(Material m) rebia indistintament un Llibre, una Revista o un Dvd, i cadascun responia segons el que realment era: el llibre cobrava 0,25 € per dia, el DVD 0,50 €, la revista 0,10 €. I tot això amb una única línia de codi de càlcul, escrita una sola vegada a Material. Aquest mecanisme s'anomena polimorfisme —del grec "moltes formes"— i és el pilar que justifica l'existència de l'herència. Sense ell, una jerarquia de classes seria poc més que una manera d'estalviar línies; amb ell, es converteix en l'eina que permet afegir tipus nous sense modificar el codi existent. Aquesta lliçó explica com funciona per dins, com es fa servir bé, i per què els if i switch sobre el tipus d'un objecte —els que omplien el teu main al mòdul 2— són un símptoma que falta polimorfisme.
Contingut
- Dos polimorfismes diferents
- Tipus declarat enfront de tipus real
- El despatx dinàmic pas a pas
- L'exemple central:
Materialque calcula multes - Upcasting: la conversió implícita
- Downcasting i
ClassCastException instanceofi el patró de Java 16- Els
ifsobre el tipus són una olor de disseny - Els membres
staticno són polimòrfics: hiding - Camps i polimorfisme: tampoc
- Polimorfisme amb arrays de la superclasse
- Errors Habituals i Consells
- Exercicis
- Dos polimorfismes diferents
En Java conviuen dues formes de polimorfisme que convé no confondre:
| Aspecte | Polimorfisme en compilació | Polimorfisme en execució |
|---|---|---|
| Un altre nom | Estàtic, ad hoc | Dinàmic, de subtipus |
| Mecanisme | Sobrecàrrega de mètodes | Sobreescriptura de mètodes |
| Qui decideix | El compilador | La JVM, en temps d'execució |
| Amb quina informació decideix | Els tipus declarats dels arguments | El tipus real de l'objecte |
| Necessita herència | No | Sí |
| Exemple | calcularMulta(int) i calcularMulta(int, double) |
Dvd.getTarifaDiaria() sobre Material.getTarifaDiaria() |
El primer ja el domines: el vas estudiar a 03-03 i no té misteri, el compilador mira els tipus escrits i tria. El segon és l'interessant i el que dona nom al pilar de la POO; quan algú diu "polimorfisme" a seques, gairebé sempre es refereix a aquest.
- Tipus declarat enfront de tipus real
Tota referència en Java té dos tipus que poden no coincidir:
| Concepte | Què és | Qui el fa servir | Quan es coneix |
|---|---|---|---|
| Tipus declarat (estàtic) | El tipus escrit a l'esquerra de la variable | El compilador, per decidir quines crides són legals | En compilació |
| Tipus real (dinàmic) | La classe de l'objecte que es va crear amb new |
La JVM, per decidir quin codi executar | En execució |
I d'aquí surten les dues regles que governen tota la resta:
El tipus declarat decideix QUÈ POTS CRIDAR. El tipus real decideix QUÈ S'EXECUTA.
Comprova-ho:
Material m = new Dvd("Curs de Spring", "DVD-0007", 240);
System.out.println(m.getTipus()); // "DVD" <-- executa la versio de Dvd
System.out.println(m.getDiesPrestec()); // 3 <-- executa la versio de Dvd
System.out.println(m.duracioMinuts); // ERROR DE COMPILACIOL'objecte és un DVD i té el seu camp duracioMinuts, però el compilador només veu un Material i Material no declara aquest camp. La restricció no és capriciosa: el compilador ha de garantir que la crida sigui vàlida per a qualsevol objecte que hi pogués haver, i en aquesta variable hi podria haver un Llibre.
- El despatx dinàmic pas a pas
El mecanisme pel qual la JVM tria la implementació correcta s'anomena despatx dinàmic (dynamic dispatch o late binding). Funciona així:
flowchart TD
A["El compilador veu
m.getTarifaDiaria()
amb m de tipus Material"] --> B{"Material declara
aquest metode?"}
B -- "no" --> E["Error de compilacio"]
B -- "si" --> C["Compila la crida
i la deixa sense resoldre"]
C --> D["En EXECUCIO la JVM
mira el tipus real de l'objecte"]
D --> F["Dvd sobreescriu
getTarifaDiaria?"]
F -- "si" --> G["Executa Dvd.getTarifaDiaria
retorna 0.50"]
F -- "no" --> H["Puja per la jerarquia
fins a trobar la versio heretada"]
En dues frases: el compilador verifica que el mètode existeix al tipus declarat, i la JVM busca la implementació començant per la classe real de l'objecte i pujant per la jerarquia fins a trobar-la. Per això Llibre, que no sobreescriu getTarifaDiaria(), acaba executant la de Material.
Aquest mecanisme té un cost mínim en rendiment (la JVM fa servir una taula de mètodes i, a més, optimitza agressivament en calent), i a canvi dona una flexibilitat enorme. Al mòdul 10-07 veuràs com el compilador JIT arriba fins i tot a eliminar aquest cost quan detecta que a la pràctica sempre s'executa la mateixa implementació.
- L'exemple central:
Material que calcula multes
Material que calcula multesAquest és l'exemple que resumeix el mòdul sencer. Fixa't en calcularMulta, escrit una sola vegada a Material:
public double calcularMulta(int diesTranscorreguts) {
int retard = Math.max(0, diesTranscorreguts - getDiesPrestec());
return Math.min(retard * getTarifaDiaria(), MULTA_MAXIMA);
}Aquest mètode no sap —ni li importa— si l'objecte és un llibre, una revista o un DVD. Crida getDiesPrestec() i getTarifaDiaria(), i el despatx dinàmic s'encarrega que cada material aporti els seus propis números.
Material m1 = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Material m2 = new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral");
Material m3 = new Dvd("Curs de Spring", "DVD-0007", 240);
System.out.printf("%-12s %-22s %8s %10s %10s %s%n",
"TIPUS", "TITOL", "TERMINI", "TARIFA", "MULTA 12d", "GRAVETAT");
mostrar(m1);
mostrar(m2);
mostrar(m3);/** Un sol metode serveix per a tots els suports, presents i futurs. */
private static void mostrar(Material m) {
System.out.printf("%-12s %-22s %6d d %9.2f %9.2f %s%n",
m.getTipus(), m.titol, m.getDiesPrestec(), m.getTarifaDiaria(),
m.calcularMulta(12), m.classificarGravetat(12));
}Sortida:
TIPUS TITOL TERMINI TARIFA MULTA 12d GRAVETAT Llibre Java Eficac 15 d 0,25 0,00 SENSE RETARD Revista Java Magazine 7 d 0,10 0,50 LLEU DVD Curs de Spring 3 d 0,50 4,50 GREU
Verifica-ho a mà: amb 12 dies transcorreguts, el llibre encara és en termini (15 dies); la revista porta 5 de retard (12 − 7) i paga 5 × 0,10 = 0,50 €, retard lleu perquè 5 ≤ 7; el DVD porta 9 de retard (12 − 3) i paga 9 × 0,50 = 4,50 €, greu perquè 9 > 7.
Tres línies de codi de negoci i tres comportaments diferents. I la propietat més valuosa: si demà Nexus Software afegeix mapes tècnics o kits de robòtica, mostrar continuarà funcionant sense tocar-lo.
- Upcasting: la conversió implícita
Assignar un objecte d'una subclasse a una referència de la superclasse s'anomena upcasting (conversió cap amunt). És implícit i sempre segur, perquè un Dvd sempre és un Material:
Dvd dvd = new Dvd("Curs de Spring", "DVD-0007", 240);
Material m = dvd; // upcasting implicit, sense sintaxi especial
// tambe passa en passar parametres...
mostrar(dvd); // el parametre es Material
// ...i en declarar directament
Material altre = new Dvd("Curs de Docker", "DVD-0008", 180);Què canvia i què no amb l'upcasting:
| Canvia | No canvia |
|---|---|
El que el compilador et deixa cridar (només el de Material) |
L'objecte: continua sent exactament el mateix Dvd |
| — | Quina implementació s'executa: la de Dvd |
| — | Els camps propis: continuen allà, només que inaccessibles per aquesta referència |
És important interioritzar que l'upcasting no transforma res: no crea cap objecte nou ni retalla l'original. Només canvia les ulleres amb què el mira el compilador.
- Downcasting i
ClassCastException
ClassCastExceptionEl downcasting és l'operació inversa: tractar una referència de la superclasse com si fos de la subclasse. Requereix sintaxi explícita, perquè no sempre és segur:
Material m = new Dvd("Curs de Spring", "DVD-0007", 240);
Dvd dvd = (Dvd) m; // downcasting explicit
System.out.println(dvd.duracioMinuts); // 240: ja s'hi pot accedirSi l'objecte real no és d'aquest tipus, el programa falla en execució:
Material m = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Dvd dvd = (Dvd) m; // compila... i peta en executarException in thread "main" java.lang.ClassCastException:
class com.nexussoftware.bibliotech.domini.Llibre cannot be cast to
class com.nexussoftware.bibliotech.domini.Dvd
at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:42)El compilador accepta la conversió perquè podria ser vàlida (un Material podria ser un Dvd), però la comprovació real passa en execució. La ClassCastException s'estudia com a excepció al mòdul 6; aquí n'hi ha prou de saber que existeix i com evitar-la: comprovant el tipus abans amb instanceof.
instanceof i el patró de Java 16
instanceof i el patró de Java 16L'operador instanceof respon la pregunta "aquest objecte és d'aquest tipus?":
Material m = obtenirMaterial();
if (m instanceof Dvd) {
Dvd dvd = (Dvd) m; // downcasting ja segur
System.out.println("Duracio: " + dvd.duracioMinuts + " min");
}Aquest patró —comprovar, convertir, declarar variable— era tan repetitiu que Java 16 el va integrar en l'operador mateix. És l'instanceof amb patró de tipus:
if (m instanceof Dvd dvd) { // comprova, converteix i declara
System.out.println("Duracio: " + dvd.duracioMinuts + " min");
}La variable dvd només existeix on el compilador pot garantir que la comprovació va ser certa, fins i tot en condicions compostes:
// Funciona: despres de && el compilador ja sap que dvd es valida
if (m instanceof Dvd dvd && dvd.duracioMinuts > 120) {
System.out.println("DVD llarg: " + dvd.titol);
}
// Tambe funciona amb negacio i sortida anticipada
if (!(m instanceof Llibre llibre)) {
return;
}
System.out.println(llibre.autor); // aqui llibre esta garantidaDos detalls útils:
null instanceof Qualsevolretornafalse, mai no llança cap error. Això converteixinstanceofen una guarda de nul·litat implícita.instanceofés cert també per a les superclasses:dvd instanceof Materialéstrue.
- Els
if sobre el tipus són una olor de disseny
if sobre el tipus són una olor de dissenyAra la part important de la lliçó. Compara aquestes dues formes de resoldre el mateix problema.
Sense polimorfisme, tal com ho hauries hagut d'escriure amb el que sabies al mòdul 2:
// ABANS: la logica de cada suport, dispersa en un switch sobre el tipus
public static double calcularMulta(String tipusMaterial, int diesTranscorreguts) {
int termini;
double tarifa;
switch (tipusMaterial) {
case "LLIBRE" -> { termini = 15; tarifa = 0.25; }
case "REVISTA" -> { termini = 7; tarifa = 0.10; }
case "DVD" -> { termini = 3; tarifa = 0.50; }
default -> { termini = 15; tarifa = 0.25; }
}
int retard = Math.max(0, diesTranscorreguts - termini);
return Math.min(retard * tarifa, 20.0);
}Amb polimorfisme:
// DESPRES: cada suport coneix les seves regles; el calcul es unic
public double calcularMulta(int diesTranscorreguts) {
int retard = Math.max(0, diesTranscorreguts - getDiesPrestec());
return Math.min(retard * getTarifaDiaria(), MULTA_MAXIMA);
}La comparació, punt per punt:
| Criteri | switch sobre el tipus |
Polimorfisme |
|---|---|---|
| Afegir un audiollibre | Modificar el switch... |
Escriure una classe nova |
| ...i tots els altres | I el de llistar, el de validar, el d'imprimir... | Res més |
| Risc d'oblidar un lloc | Alt: els switch es dispersen pel codi |
Nul: el compilador reuneix tot a la classe |
| On és la regla del DVD | Repartida per N mètodes | A Dvd.java, i només allà |
| Què passa amb un tipus desconegut | Cau al default, silenciosament |
No hi ha cas desconegut |
El criteri professional, que convé memoritzar:
Si escrius un
ifo unswitchque pregunta de quin tipus és un objecte per decidir què fer, gairebé sempre falta un mètode polimòrfic.
Això no vol dir que instanceof estigui prohibit. Hi ha usos legítims:
- Implementar
equals(lliçó 03-09). - Treballar amb API alienes la jerarquia de les quals no pots modificar.
- Recuperar una dada específica d'un subtipus per presentar-la, com el
duracioMinutsd'un DVD en una fitxa detallada, quan aquesta dada no té equivalent en els altres.
El cas il·legítim és el que substitueix un comportament que hauria de viure a les classes:
// MALAMENT: comportament de negoci decidit des de fora
double tarifa;
if (m instanceof Llibre) tarifa = 0.25;
else if (m instanceof Revista) tarifa = 0.10;
else if (m instanceof Dvd) tarifa = 0.50;
else tarifa = 0.25;
// BE
double tarifa = m.getTarifaDiaria();
- Els membres
static no són polimòrfics: hiding
static no són polimòrfics: hidingAquí arriba un parany que cal conèixer per no caure-hi. Els mètodes static no se sobreescriuen: s'amaguen (hiding), i es resolen amb el tipus declarat, no amb el tipus real.
public class Material {
public static String descripcioTipus() {
return "Material generic";
}
}
public class Dvd extends Material {
public static String descripcioTipus() { // AMAGA, no sobreescriu
return "DVD de formacio";
}
}Material m = new Dvd("Curs de Spring", "DVD-0007", 240);
System.out.println(m.descripcioTipus()); // "Material generic" (!)
System.out.println(Material.descripcioTipus()); // "Material generic"
System.out.println(Dvd.descripcioTipus()); // "DVD de formacio"La primera línia és la sorprenent. L'objecte és un Dvd, però com que descripcioTipus() és static, la JVM no fa despatx dinàmic: fa servir el tipus declarat de m, que és Material. És exactament el contrari del que fan els mètodes d'instància.
Contrast directe, en una taula:
| Mètode d'instància | Mètode static |
|
|---|---|---|
| Redefinir-lo a la subclasse s'anomena | Sobreescriptura (override) | Ocultació (hiding) |
| Es resol amb | Tipus real de l'objecte | Tipus declarat de la referència |
Admet @Override |
Sí | No (error de compilació) |
| És polimòrfic | Sí | No |
Prova de foc: si afegeixes @Override al mètode static de Dvd, el compilador falla amb "method does not override or implement a method from a supertype". Una raó més per escriure sempre l'anotació.
Consell pràctic: no cridis mai un mètode static a través d'una referència (m.descripcioTipus()). Escriu sempre Material.descripcioTipus() o Dvd.descripcioTipus(), i l'ambigüitat desapareix del codi.
- Camps i polimorfisme: tampoc
La mateixa regla s'aplica als camps: els camps no són polimòrfics, es resolen amb el tipus declarat.
public class Material {
public String etiqueta = "MATERIAL";
}
public class Dvd extends Material {
public String etiqueta = "DVD"; // AMAGA el camp de Material
}Dvd dvd = new Dvd("Curs de Spring", "DVD-0007", 240);
Material mat = dvd; // la MATEIXA caixa de memoria
System.out.println(dvd.etiqueta); // "DVD"
System.out.println(mat.etiqueta); // "MATERIAL" <-- mateix objecteL'objecte conté tots dos camps alhora, i quin veus depèn de amb quines ulleres el miris. És una font d'errors tan absurda que la solució és senzilla: no declaris en una subclasse un camp amb el mateix nom que un de la superclasse. I si el que vols és un valor que variï per tipus, fes servir un mètode sobreescrivible, com getTipus() a BiblioTech.
- Polimorfisme amb arrays de la superclasse
El polimorfisme brilla quan tractes molts objectes de manera uniforme. Per a això cal poder guardar-los junts, i de moment l'única eina disponible és un array:
Nota provisional. Els arrays tenen mida fixa i són incòmodes per a un catàleg real: no s'hi pot afegir ni eliminar sense crear un altre array. Al mòdul 5 apareixeran
ArrayList,HashMapi la resta del framework de col·leccions, que és la solució definitiva. Fes servir l'array aquí com a bastida per veure el polimorfisme en acció.
Material[] cataleg = {
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994),
new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 2018),
new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral"),
new Dvd("Curs de Spring", "DVD-0007", 240)
};
System.out.printf("%-12s %-22s %8s %10s %s%n",
"TIPUS", "TITOL", "TERMINI", "MULTA 12d", "GRAVETAT");
double recaptacioPrevista = 0.0;
for (Material m : cataleg) { // una sola linia de codi...
System.out.printf("%-12s %-22s %6d d %9.2f %s%n",
m.getTipus(), m.titol, m.getDiesPrestec(),
m.calcularMulta(12), m.classificarGravetat(12));
recaptacioPrevista += m.calcularMulta(12); // ...i cada objecte aplica el seu
}
System.out.printf("%nRecaptacio prevista a 12 dies: %.2f EUR%n", recaptacioPrevista);Sortida:
TIPUS TITOL TERMINI MULTA 12d GRAVETAT Llibre Java Eficac 15 d 0,00 SENSE RETARD Llibre Patrons de Disseny 15 d 0,00 SENSE RETARD Llibre Refactoritzacio 15 d 0,00 SENSE RETARD Revista Java Magazine 7 d 0,50 LLEU DVD Curs de Spring 3 d 4,50 GREU Recaptacio prevista a 12 dies: 5,00 EUR
Un array de Material pot contenir objectes de qualsevol subclasse, i el bucle els tracta a tots igual mentre cadascun fa el seu. Aquest és, en essència, el motiu pel qual existeix la POO al programari gran.
Un detall a tenir en compte: en recórrer l'array, el tipus declarat de m és Material, així que només pots cridar el que Material declari. Si necessites l'autor dels llibres, et caldrà instanceof amb patró:
for (Material m : cataleg) {
if (m instanceof Llibre llibre) {
System.out.println(llibre.titol + " es de " + llibre.autor);
}
}I aquest és exactament un dels usos legítims de l'apartat 8: extreure una dada que només existeix en un subtipus, no decidir comportament de negoci.
Errors Habituals i Consells
- Creure que l'upcasting "converteix" l'objecte. No el toca. Només limita el que el compilador et deixa cridar. L'objecte continua sent el que era, i els seus mètodes sobreescrits continuen executant-se.
- Fer downcasting sense comprovar.
(Dvd) msense uninstanceofal davant és unaClassCastExceptionesperant el seu moment. Fes servir sempreif (m instanceof Dvd dvd). - Posar
@Overridea un mètodestatic. No compila, i aquesta és la seva virtut: t'avisa que no estàs sobreescrivint, sinó amagant. - Cridar mètodes
statica través d'una instància.m.descripcioTipus()enganya sobre què s'executarà. Fes servir el nom de la classe. - Redefinir un camp a la subclasse. Acabes amb dos camps amb el mateix nom al mateix objecte i resultats que depenen del tipus declarat. No ho facis.
- Cadenes d'
else if (x instanceof ...). Gairebé sempre volen dir que falta un mètode a la jerarquia. Abans d'escriure la tercera branca, pregunta't quin mètode hauria de tenirMaterial. - Consell: quan afegeixis una subclasse, prova de llistar quins mètodes sobreescriu. Si no en sobreescriu cap, potser no necessitaves una subclasse, sinó un camp.
- Consell: programa contra el tipus més general que et serveixi. Declara
Material men lloc deDvd dquan no necessitis l'específic del DVD: el teu codi valdrà per a més casos. Aquest principi es porta a l'extrem al mòdul 4 amb les interfícies. - Consell: fes servir la marca del marge de l'IDE. IntelliJ i Eclipse mostren una icona al costat dels mètodes sobreescrits i permeten saltar a la implementació real amb un clic.
Exercicis
Exercici 1: afegir un tipus sense tocar el codi existent
Aquest exercici comprova la propietat més valuosa del polimorfisme. Nexus Software incorpora kits de robòtica al catàleg: es presten 5 dies, amb tarifa d'1,00 €/dia (són cars), i tenen un camp nombrePeces.
- Crea
KitRoboticacom a subclasse deMaterial. - Afegeix-lo a l'array
catalegde l'apartat 11. - Executa sense modificar ni una línia de
Material,Llibre,Revista,Dvd,mostrarni el bucle de llistat. - Verifica manualment la multa a 12 dies transcorreguts i la gravetat.
Després respon: quants fitxers has hagut de tocar? Quants n'hauries tocat amb el switch de l'apartat 8?
Exercici 2: refactoritzar un switch sobre el tipus
Un company ha escrit aquest mètode a BiblioTechApp. Refactoritza'l eliminant per complet el switch, movent cada responsabilitat a la classe que li correspon. Indica quins mètodes nous necessita Material i quins sobreescriu cada subclasse.
public static String generarAvis(Material m, int diesTranscorreguts) {
String canal;
int diesPreavis;
switch (m.getTipus()) {
case "Llibre" -> { canal = "correu"; diesPreavis = 3; }
case "Revista" -> { canal = "xat"; diesPreavis = 1; }
case "DVD" -> { canal = "telefon"; diesPreavis = 1; }
default -> { canal = "correu"; diesPreavis = 3; }
}
return "Avis per " + canal + " amb " + diesPreavis
+ " dies de preavis. Multa actual: "
+ m.calcularMulta(diesTranscorreguts) + " EUR";
}Exercici 3: demostrar hiding enfront d'overriding
Escriu una classe de prova que demostri en la mateixa execució les tres regles dels apartats 9 i 10:
- Un mètode d'instància sobreescrit executa la versió del tipus real.
- Un mètode
staticredefinit executa la versió del tipus declarat. - Un camp redefinit es llegeix segons el tipus declarat, encara que l'objecte sigui el mateix.
Imprimeix en cada cas el tipus declarat, el tipus real (amb getClass().getSimpleName()) i el valor obtingut, i explica per escrit per què el resultat 1 difereix dels resultats 2 i 3.
Solucions
Solució 1
package com.nexussoftware.bibliotech.domini;
/** Kit de robotica per a formacio practica. Material car i escas. */
public class KitRobotica extends Material {
public static final int DIES_PRESTEC_KIT = 5;
public static final double TARIFA_DIARIA_KIT = 1.00;
public final int nombrePeces;
public KitRobotica(String titol, String referencia, int nombrePeces) {
super(titol, referencia, true);
this.nombrePeces = Math.max(0, nombrePeces);
}
@Override
public int getDiesPrestec() { return DIES_PRESTEC_KIT; }
@Override
public double getTarifaDiaria() { return TARIFA_DIARIA_KIT; }
@Override
public String getTipus() { return "Kit"; }
@Override
public String descriure() {
return super.descriure() + " - " + nombrePeces + " peces";
}
}Només cal afegir-lo a l'array:
Material[] cataleg = {
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994),
new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 2018),
new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral"),
new Dvd("Curs de Spring", "DVD-0007", 240),
new KitRobotica("Kit Arduino Avancat", "KIT-0001", 128) // unica linia nova
};Sortida (fila nova marcada):
TIPUS TITOL TERMINI MULTA 12d GRAVETAT Llibre Java Eficac 15 d 0,00 SENSE RETARD Llibre Patrons de Disseny 15 d 0,00 SENSE RETARD Llibre Refactoritzacio 15 d 0,00 SENSE RETARD Revista Java Magazine 7 d 0,50 LLEU DVD Curs de Spring 3 d 4,50 GREU Kit Kit Arduino Avancat 5 d 7,00 LLEU Recaptacio prevista a 12 dies: 12,00 EUR
Verificació manual, parant atenció a la frontera: 12 − 5 = 7 dies de retard; 7 × 1,00 = 7,00 €, per sota del sostre de 20 €. I la gravetat és LLEU, no GREU, perquè la condició és retard <= LLINDAR_LLEU i LLINDAR_LLEU val exactament 7. És just el tipus de valor límit que la lliçó 02-05 et va ensenyar a comprovar: els llindars es proven sempre amb els tres valors frontera (6, 7 i 8), perquè un < en lloc d'un <= canviaria aquest resultat sense que cap altra fila de la taula se n'assabentés.
Resposta a la pregunta. Has tocat dos fitxers: un de nou (KitRobotica.java) i una línia a BiblioTechApp per donar-lo d'alta. Amb el switch de l'apartat 8 hauries hagut de localitzar i modificar tots els mètodes que continguessin un switch sobre el tipus —càlcul de multa, llistat, avisos, validació—, amb el risc d'oblidar-ne algun i que el kit acabés tractat com un llibre sense que ningú se n'assabentés.
Solució 2
El switch decideix dues coses —el canal d'avís i els dies de preavís— que són propietats de cada suport. El seu lloc són les classes.
A Material, dos mètodes nous amb el valor per defecte, i un tercer que compon el missatge una sola vegada:
/** @return canal preferent per avisar del venciment d'aquest material. */
public String getCanalAvis() {
return "correu";
}
/** @return dies d'antelacio amb que s'avisa del venciment. */
public int getDiesPreavis() {
return 3;
}
/** @return text de l'avis de venciment per a aquest material. */
public String generarAvis(int diesTranscorreguts) {
return String.format("Avis per %s amb %d dies de preavis. Multa actual: %.2f EUR",
getCanalAvis(), getDiesPreavis(),
calcularMulta(diesTranscorreguts));
}A Revista:
@Override
public String getCanalAvis() { return "xat"; }
@Override
public int getDiesPreavis() { return 1; }A Dvd:
@Override
public String getCanalAvis() { return "telefon"; }
@Override
public int getDiesPreavis() { return 1; }Llibre no sobreescriu res: hereta "correu" i 3 dies, que eren justament el cas default del switch original. I BiblioTechApp es queda sense el mètode:
Resultats:
| Material | Abans (switch) | Després (polimorfisme) |
|---|---|---|
Llibre |
correu, 3 dies | correu, 3 dies (heretat) |
Revista |
xat, 1 dia | xat, 1 dia (sobreescrit) |
Dvd |
telèfon, 1 dia | telèfon, 1 dia (sobreescrit) |
KitRobotica |
queia al default sense que ningú ho decidís |
hereta el valor per defecte explícitament, o el sobreescriu |
Tres guanys concrets: el default silenciós desapareix, cada suport reuneix totes les seves regles al seu fitxer, i afegir un tipus nou ja no obliga a recórrer el codi buscant switch oblidats.
Solució 3
package com.nexussoftware.bibliotech;
class Base {
public String camp = "camp de Base";
public String metodeInstancia() { return "instancia de Base"; }
public static String metodeStatic() { return "static de Base"; }
}
class Derivada extends Base {
public String camp = "camp de Derivada"; // AMAGA el de Base
@Override
public String metodeInstancia() { return "instancia de Derivada"; }
public static String metodeStatic() { return "static de Derivada"; } // AMAGA
}
public class DemoPolimorfisme {
public static void main(String[] args) {
Derivada d = new Derivada();
Base b = d; // upcasting: MATEIX objecte, altres ulleres
System.out.println("Tipus declarat de b: Base");
System.out.println("Tipus real de b: " + b.getClass().getSimpleName());
System.out.println("b == d: " + (b == d));
System.out.println();
// 1. Metode d'instancia: guanya el TIPUS REAL
System.out.println("1) b.metodeInstancia() -> " + b.metodeInstancia());
System.out.println(" d.metodeInstancia() -> " + d.metodeInstancia());
// 2. Metode static: guanya el TIPUS DECLARAT
System.out.println("2) b.metodeStatic() -> " + b.metodeStatic());
System.out.println(" d.metodeStatic() -> " + d.metodeStatic());
// 3. Camp: guanya el TIPUS DECLARAT
System.out.println("3) b.camp -> " + b.camp);
System.out.println(" d.camp -> " + d.camp);
}
}Sortida:
Tipus declarat de b: Base Tipus real de b: Derivada b == d: true 1) b.metodeInstancia() -> instancia de Derivada d.metodeInstancia() -> instancia de Derivada 2) b.metodeStatic() -> static de Base d.metodeStatic() -> static de Derivada 3) b.camp -> camp de Base d.camp -> camp de Derivada
Explicació. La línia b == d dona true: hi ha un sol objecte i dues referències, així que qualsevol diferència entre les tres sortides ve del tipus declarat, no de l'objecte.
El cas 1 fa servir despatx dinàmic: els mètodes d'instància es resolen en execució mirant la classe real, que és Derivada en tots dos casos. És l'únic dels tres que és polimòrfic.
Els casos 2 i 3 es resolen en compilació. El compilador substitueix b.metodeStatic() per Base.metodeStatic() i b.camp pel camp declarat a Base, perquè aquest és el tipus amb què es va declarar la variable. La JVM ni tan sols arriba a preguntar-se quin objecte hi ha al darrere. Per això el mateix objecte exhibeix dos valors diferents per a camp: l'objecte conté tots dos camps, i cada referència llegeix el seu.
La moralitat pràctica dels tres casos junts: el polimorfisme és cosa exclusiva dels mètodes d'instància. Qualsevol altre mecanisme —static, camps— es resol pel tipus escrit i produeix sorpreses si intentes fer-lo servir com si fos polimòrfic.
Conclusió
Has arribat al nucli de l'orientació a objectes. Distingeixes els dos polimorfismes —la sobrecàrrega, que resol el compilador, i la sobreescriptura, que resol la JVM— i comprens la regla que els governa: el tipus declarat decideix què pots cridar, el tipus real decideix què s'executa. Has seguit el despatx dinàmic pas a pas i n'has vist la conseqüència pràctica a l'exemple central del mòdul: un únic calcularMulta a Material que cobra 0,25 € a un llibre, 0,10 € a una revista i 0,50 € a un DVD, sense ni un sol if sobre el tipus. Domines l'upcasting implícit i segur, el downcasting explícit i arriscat amb la seva ClassCastException, i la forma moderna de protegir-te amb instanceof i patró de tipus (if (m instanceof Dvd dvd)), que en una línia comprova, converteix i declara. Saps reconèixer els if/switch sobre el tipus com una olor de disseny i refactoritzar-los movent cada regla a la classe que li correspon. I coneixes les dues excepcions que cal tenir sempre presents: els mètodes static no se sobreescriuen, s'amaguen, i els camps tampoc no són polimòrfics; tots dos es resolen pel tipus declarat.
Sobretot, has comprovat a la pràctica la propietat que justifica tot l'entramat: afegir un kit de robòtica al catàleg t'ha costat una classe nova i una línia, sense tocar res del que ja funcionava.
Queda una debilitat greu al disseny actual, i l'hauràs notada: tots els camps continuen sent public. Qualsevol pot escriure llibre.disponible = true saltant-se els avisos de prestar(), i res no impedeix assignar a un préstec una multa inventada. A la lliçó següent, Encapsulament, tancaràs aquesta porta: faràs privats els camps de Llibre, Empleat i Prestec, aprendràs la taula completa de modificadors d'accés, veuràs per què posar un setter a cada camp destrueix precisament el que es pretenia protegir, i descobriràs la immutabilitat i les còpies defensives com a eines perquè l'estat dels teus objectes sigui, de debò, seu.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
