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

  1. Dos polimorfismes diferents
  2. Tipus declarat enfront de tipus real
  3. El despatx dinàmic pas a pas
  4. L'exemple central: Material que calcula multes
  5. Upcasting: la conversió implícita
  6. Downcasting i ClassCastException
  7. instanceof i el patró de Java 16
  8. Els if sobre el tipus són una olor de disseny
  9. Els membres static no són polimòrfics: hiding
  10. Camps i polimorfisme: tampoc
  11. Polimorfisme amb arrays de la superclasse
  12. Errors Habituals i Consells
  13. Exercicis

  1. 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
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.

  1. Tipus declarat enfront de tipus real

Tota referència en Java té dos tipus que poden no coincidir:

Material m = new Dvd("Curs de Spring", "DVD-0007", 240);
//       ^                 ^
//  tipus declarat     tipus real
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 COMPILACIO
error: cannot find symbol
  symbol:   variable duracioMinuts
  location: variable m of type Material

L'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.

  1. 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ó.

  1. L'exemple central: Material que calcula multes

Aquest é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.

  1. 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.

  1. Downcasting i ClassCastException

El 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 accedir

Si 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 executar
Exception 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.

  1. instanceof i el patró de Java 16

L'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 garantida

Dos detalls útils:

  • null instanceof Qualsevol retorna false, mai no llança cap error. Això converteix instanceof en una guarda de nul·litat implícita.
  • instanceof és cert també per a les superclasses: dvd instanceof Material és true.

  1. Els if sobre el tipus són una olor de disseny

Ara 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 if o un switch que 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 duracioMinuts d'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();

  1. Els membres static no són polimòrfics: hiding

Aquí 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 No (error de compilació)
És polimòrfic 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.

  1. 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 objecte

L'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.

  1. 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, HashMap i 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) m sense un instanceof al davant és una ClassCastException esperant el seu moment. Fes servir sempre if (m instanceof Dvd dvd).
  • Posar @Override a un mètode static. No compila, i aquesta és la seva virtut: t'avisa que no estàs sobreescrivint, sinó amagant.
  • Cridar mètodes static a 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 tenir Material.
  • 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 m en lloc de Dvd d quan 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.

  1. Crea KitRobotica com a subclasse de Material.
  2. Afegeix-lo a l'array cataleg de l'apartat 11.
  3. Executa sense modificar ni una línia de Material, Llibre, Revista, Dvd, mostrar ni el bucle de llistat.
  4. 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:

  1. Un mètode d'instància sobreescrit executa la versió del tipus real.
  2. Un mètode static redefinit executa la versió del tipus declarat.
  3. 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:

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

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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats