Nexus Software acaba de comunicar una novetat a BiblioTech: la biblioteca tècnica no presta només llibres. També presta revistes especialitzades —que es retornen en una setmana perquè circulen molt— i DVD de formació, que es presten tres dies i la tarifa de retard dels quals és el doble, perquè hi ha poques còpies. Amb el que saps fins ara, l'única sortida seria duplicar la classe Llibre dues vegades, canviant dos números i el nom: tres classes gairebé idèntiques, amb la mateixa lògica de disponibilitat copiada tres cops i tres llocs on corregir cada error. L'herència és el mecanisme amb què Java evita exactament això: permet definir una classe com una especialització d'una altra, heretant tot el que ja té i declarant únicament allò en què difereix. És el pilar més potent de la POO i també el més fàcil de fer servir malament, així que aquesta lliçó t'ensenya tant a aplicar-lo com a saber quan no aplicar-lo.

Contingut

  1. La relació "és un"
  2. extends: sintaxi i primer exemple
  3. Què s'hereta i què no
  4. super: el constructor de la superclasse
  5. super: accedir a membres de la superclasse
  6. Ordre de construcció en una jerarquia
  7. Sobreescriptura de mètodes i @Override
  8. Sobreescriure enfront de sobrecarregar
  9. final en classes i mètodes
  10. Tota classe hereta d'Object
  11. Herència enfront de composició
  12. Jerarquies profundes i la fragilitat de la classe base
  13. BiblioTech: la jerarquia de materials prestables
  14. Errors Habituals i Consells
  15. Exercicis

  1. La relació "és un"

L'herència modela una relació molt concreta: "X és un Y". Si la frase sona natural i és certa sempre, l'herència és candidata:

  • Un llibre és un material prestable. ✔
  • Una revista és un material prestable. ✔
  • Un DVD és un material prestable. ✔
  • Un préstec és un llibre. ✘ (un préstec té un llibre)
  • Un empleat és un llibre. ✘

Vocabulari:

Terme Sinònims Significat
Superclasse classe base, classe mare La classe general de la qual s'hereta
Subclasse classe derivada, classe filla La classe especialitzada que hereta
Herència extensió El mecanisme que les uneix

I una regla que Java imposa i convé conèixer des del principi: una classe només pot estendre una superclasse. No hi ha herència múltiple de classes (sí que n'hi ha d'interfícies, lliçó 04-01). Aquesta restricció evita ambigüitats famoses d'altres llenguatges.

  1. extends: sintaxi i primer exemple

public class Material {
    public String titol;
    public boolean disponible;

    public void prestar() {
        disponible = false;
    }
}
public class Revista extends Material {
    public int numero;          // camp propi, a mes dels heretats
}

Amb només aquesta línia, Revista disposa de titol, disponible i prestar() sense haver-los escrit:

Revista r = new Revista();
r.titol      = "Java Magazine";   // heretat
r.numero     = 42;                // propi
r.prestar();                      // heretat
System.out.println(r.disponible); // false

La subclasse és igual o més gran que la superclasse: mai menys. Pot afegir camps, afegir mètodes i canviar el comportament dels heretats, però no pot eliminar res.

  1. Què s'hereta i què no

Una taula que resol la majoria dels dubtes:

Element de la superclasse S'hereta? Matís
Camps public i protected Accessibles directament des de la subclasse
Camps private Existeixen, però no són accessibles L'objecte els conté; la subclasse ha de fer servir mètodes per arribar-hi
Camps i mètodes de paquet (default) Només si la subclasse és al mateix paquet Vegeu 03-07
Mètodes public i protected Es poden sobreescriure
Mètodes static S'hereten, però no se sobreescriuen S'"amaguen" (lliçó 03-06)
Constructors No Cal declarar-los a cada subclasse, encara que es poden invocar amb super(...)
Blocs d'inicialització S'executen com a part de la construcció No s'"hereten" com a membres

El punt que més confon és el dels camps private. Un objecte Revista sí que conté els camps privats de Material —ocupen memòria i s'inicialitzen—, però el codi de Revista no els pot anomenar. Ha de fer servir els mètodes que la superclasse exposi. No és una limitació capriciosa: és encapsulament (03-07), i permet canviar la representació interna de Material sense trencar les seves filles.

L'altre punt és el dels constructors: no s'hereten. Si Material té un constructor amb tres paràmetres, new Revista(a, b, c) no funciona per art de màgia; Revista ha de declarar el seu propi constructor.

  1. super: el constructor de la superclasse

super(...) invoca un constructor de la superclasse. I és obligatori en el sentit següent:

Tot constructor d'una subclasse comença cridant un constructor de la seva superclasse. Si no l'escrius tu, el compilador insereix una crida implícita a super() sense arguments.

public class Material {
    public final String titol;
    public final String referencia;
    public boolean disponible;

    public Material(String titol, String referencia) {
        this.titol      = titol;
        this.referencia = referencia;
        this.disponible = true;
    }
}
public class Revista extends Material {
    public final int numero;

    public Revista(String titol, String referencia, int numero) {
        super(titol, referencia);   // PRIMERA sentencia, obligatoria aqui
        this.numero = numero;
    }
}

Regles, bessones de les de this(...) que vas veure a 03-04:

  1. super(...) ha de ser la primera sentència del constructor.
  2. No poden coexistir super(...) i this(...) al mateix constructor: si fas servir this(...), la cadena acabarà en algun constructor que cridi super(...).
  3. Si la superclasse no té constructor sense arguments i la subclasse no crida explícitament super(...), hi ha error de compilació:
public class Revista extends Material {
    public Revista() { }    // ERROR
}
error: constructor Material in class Material cannot be applied to given types;
  required: String,String
  found:    no arguments

Aquest error, molt freqüent, té una lectura clara: "has creat una subclasse d'una cosa que exigeix títol i referència, però no els hi estàs donant".

  1. super: accedir a membres de la superclasse

super també serveix, fora dels constructors, per arribar a un membre de la superclasse que la subclasse ha sobreescrit:

public class Material {
    public String descriure() {
        return titol + " (ref. " + referencia + ")";
    }
}

public class Revista extends Material {
    public int numero;

    @Override
    public String descriure() {
        return super.descriure() + " - numero " + numero;   // reutilitza i amplia
    }
}

Aquest patró —cridar super.metode() i afegir-hi alguna cosa— és un dels usos més valuosos de l'herència: no dupliques la lògica de la superclasse, l'estens.

Revista r = new Revista("Java Magazine", "REV-2024-42", 42);
System.out.println(r.descriure());
// Java Magazine (ref. REV-2024-42) - numero 42

  1. Ordre de construcció en una jerarquia

Quan crees un objecte d'una subclasse, la construcció va de dalt a baix: primer es construeix la part heretada, després la pròpia. Comprova-ho amb traces:

public class Material {
    public Material() {
        System.out.println("2. Constructor de Material");
    }
    { System.out.println("1. Bloc d'instancia de Material"); }
}

public class Llibre extends Material {
    public Llibre() {
        super();                                   // implicit si no s'escriu
        System.out.println("4. Constructor de Llibre");
    }
    { System.out.println("3. Bloc d'instancia de Llibre"); }
}
new Llibre();

Sortida:

1. Bloc d'instancia de Material
2. Constructor de Material
3. Bloc d'instancia de Llibre
4. Constructor de Llibre
flowchart TD
    A["new Llibre()"] --> B["Constructor de Llibre
    crida super() com a primera sentencia"]
    B --> C["Blocs i inicialitzadors de Material"]
    C --> D["Cos del constructor de Material"]
    D --> E["Blocs i inicialitzadors de Llibre"]
    E --> F["Cos del constructor de Llibre"]
    F --> G["Objecte complet"]

Aquest ordre explica el consell que va quedar pendent a 03-04: no cridis mètodes sobreescrivibles des d'un constructor. Si Material cridés al seu constructor un mètode que Llibre sobreescriu, s'executaria el codi de Llibre abans que els camps de Llibre estiguessin inicialitzats, i aquest veuria null i zeros. Demostració:

public class Material {
    public Material() {
        System.out.println("Material construeix: " + descriure());   // PERILL
    }
    public String descriure() { return "material generic"; }
}

public class Llibre extends Material {
    private final String autor;

    public Llibre(String autor) {
        super();
        this.autor = autor;
    }

    @Override
    public String descriure() { return "llibre de " + autor; }
}
new Llibre("Joshua Bloch");
// Material construeix: llibre de null      <-- autor encara no assignat

El camp autor és final i acabarà valent "Joshua Bloch", però en l'instant de la crida encara val null. Regla pràctica: des d'un constructor, crida només mètodes private, static o final.

  1. Sobreescriptura de mètodes i @Override

Sobreescriure (override) és redefinir a la subclasse un mètode heretat, amb la mateixa signatura, per canviar-ne el comportament.

public class Material {
    public int getDiesPrestec() {
        return 15;
    }
}

public class Dvd extends Material {
    @Override
    public int getDiesPrestec() {
        return 3;                 // els DVD circulen mes rapid
    }
}

Regles d'una sobreescriptura vàlida:

Element Regla
Nom i paràmetres Idèntics (mateixa signatura)
Tipus de retorn Igual, o un subtipus de l'original (retorn covariant)
Visibilitat Igual o més permissiva (protectedpublic sí; publicprivate no)
Mètode final No es pot sobreescriure
Mètode static No se sobreescriu, s'amaga (03-06)
Mètode private No s'hereta, així que no se sobreescriu

L'anotació @Override no és obligatòria, però l'has de posar sempre. La seva funció és demanar al compilador que verifiqui que realment estàs sobreescrivint alguna cosa. Sense ella, un error tipogràfic es converteix en un mètode nou que ningú no crida:

public class Dvd extends Material {
    public int getDiesPrestecs() {     // compte! "Prestecs" amb S
        return 3;
    }
}

Aquest codi compila perfectament i no sobreescriu res: els DVD continuaran tenint 15 dies de termini, i descobrir per què et costarà una tarda. Amb @Override al davant, el compilador falla immediatament:

error: method does not override or implement a method from a supertype

Una anotació que s'escriu en un segon i evita tota una classe d'errors silenciosos. Les anotacions en general s'estudien a la lliçó 10-02.

  1. Sobreescriure enfront de sobrecarregar

Aquesta és una de les confusions més persistents de qui aprèn Java, agreujada pel fet que els termes sonen molt semblants. La taula ho deixa clar:

Aspecte Sobrecàrrega (overload) Sobreescriptura (override)
On passa A la mateixa classe (o heretant) Entre superclasse i subclasse
Signatura Diferent llista de paràmetres Idèntica
Nom Igual Igual
Tipus de retorn Pot canviar lliurement Igual o subtipus
Quan es decideix En compilació, segons el tipus declarat En execució, segons l'objecte real
Anotació Cap @Override
Per a què serveix Oferir variants de la mateixa operació Especialitzar el comportament heretat
Exemple calcularMulta() i calcularMulta(int) Dvd.getDiesPrestec() sobre Material.getDiesPrestec()

Exemple amb totes dues coses alhora:

public class Material {
    public double calcularMulta(int diesRetard) { return diesRetard * 0.25; }
}

public class Dvd extends Material {

    @Override
    public double calcularMulta(int diesRetard) {      // SOBREESCRIPTURA
        return diesRetard * 0.50;
    }

    public double calcularMulta(int diesRetard, double descompte) {   // SOBRECARREGA
        return calcularMulta(diesRetard) * (1 - descompte);
    }
}

La fila més important de la taula és la de "quan es decideix": la sobrecàrrega la resol el compilador mirant els tipus escrits al codi; la sobreescriptura la resol la JVM en execució mirant l'objecte real. Aquesta diferència és el cor del polimorfisme, i és el tema de la lliçó següent.

  1. final en classes i mètodes

final té tres usos en Java, i ja en coneixes dos:

Aplicat a Significa
Variable o camp No es pot reassignar (mòduls 1 i 03-04)
Mètode No es pot sobreescriure en cap subclasse
Classe No es pot estendre: no admet subclasses
public class Material {
    /** El calcul del sostre legal no es pot alterar en cap subclasse. */
    public final double aplicarSostre(double quantitat) {
        return Math.min(quantitat, MULTA_MAXIMA);
    }
}
public final class Prestec {   // ningu no pot crear "SubPrestec"
    // ...
}

Per a què serveix? Per protegir invariants. Si una regla s'ha de complir sempre, sense excepció, un mètode final impedeix que una subclasse se la salti. Les classes final més famoses de Java són String i Integer: si poguessis estendre String i sobreescriure'n els mètodes, la seguretat de tot el llenguatge se n'aniria en orris.

El consell professional, formulat per Joshua Bloch a Java Eficaç (sí, el llibre del catàleg de BiblioTech): dissenya per a l'herència i documenta-la, o prohibeix-la. Una classe que no s'ha pensat explícitament per ser estesa és millor declarar-la final.

  1. Tota classe hereta d'Object

Una regla del llenguatge que probablement ja has notat: si una classe no declara extends, hereta implícitament de java.lang.Object.

public class Material { }
// equival exactament a
public class Material extends Object { }

Per això pots escriure això sense haver definit res:

Llibre l = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(l.toString());   // com.nexussoftware...Llibre@1b6d3586
System.out.println(l.equals(l));    // true
System.out.println(l.hashCode());   // 460141958

La jerarquia real de BiblioTech, per tant, és aquesta:

classDiagram
    Object <|-- Material
    Material <|-- Llibre
    Material <|-- Revista
    Material <|-- Dvd
    Object <|-- Empleat
    Object <|-- Prestec
    class Object {
        +toString() String
        +equals(Object) boolean
        +hashCode() int
        +getClass() Class
    }

Els mètodes que Object regala funcionen, però el seu comportament per defecte rarament és el que vols: toString() imprimeix un críptic Llibre@1b6d3586 i equals compara adreces de memòria, de manera que dos llibres amb el mateix ISBN són diferents. Sobreescriure'ls correctament és el tema de la lliçó 03-09, que tanca aquest mòdul.

  1. Herència enfront de composició

L'herència és tan còmoda que se n'abusa. L'alternativa és la composició: en lloc de "ser un", l'objecte té un col·laborador a qui delega la feina.

Criteri Herència Composició
Relació "és un" "té un"
Sintaxi class B extends A class B { private A a; }
Acoblament Fort: B depèn dels detalls interns d'A Feble: B només fa servir l'API pública d'A
Es decideix en Compilació (no canvia mai) Execució (es pot substituir el col·laborador)
Reutilitza Tota la implementació d'A, vulgui o no Només el que decideixi fer servir
Risc Canvis a A trenquen B Baix

Un cas on l'herència seria un error. Imagina que vols que Prestec reutilitzi el codi de Llibre per no repetir el títol i la referència, i escrius:

public class Prestec extends Llibre {   // ERROR DE DISSENY
    public Empleat empleat;
    public int diesTranscorreguts;
}

Compila. I és un desastre, per tres raons:

  1. La frase és falsa. Un préstec no és un llibre; un préstec un llibre. Si la frase "és un" sona estranya en dir-la en veu alta, no facis servir herència.
  2. Hereta operacions absurdes. prestec.prestar() i prestec.retornar() passen a existir, i no signifiquen res. Estàs ampliant la superfície pública amb mètodes sense sentit.
  3. Trenca el polimorfisme. Qualsevol codi que rebi un Llibre acceptaria un Prestec, i un catàleg de llibres podria acabar contenint préstecs.

La forma correcta és la que ja tens:

public class Prestec {           // sense extends
    public final Material material;    // TE un material
    public final Empleat empleat;      // TE un empleat
}

Regla pràctica que se cita constantment a la indústria: prefereix composició a herència. Fes servir herència només quan la subclasse sigui genuïnament substituïble per la superclasse en qualsevol context —el que es coneix com a principi de substitució de Liskov— i quan vulguis aprofitar el polimorfisme.

  1. Jerarquies profundes i la fragilitat de la classe base

Dos problemes que apareixen en projectes reals i que convé anticipar:

Jerarquies profundes. Cadenes com Object → Material → MaterialFisic → MaterialImpres → Publicacio → PublicacioPeriodica → Revista fan que entendre Revista exigeixi llegir sis fitxers, que un canvi a qualsevol nivell es propagui cap avall i que sigui impossible saber d'on surt un mètode concret. Criteri sa: dos o tres nivells com a màxim; si en necessites més, probablement el que busques és composició o interfícies (04-01).

Fragilitat de la classe base (fragile base class problem). Una subclasse depèn no només de què fa la superclasse, sinó de com ho fa. Un canvi intern inofensiu la pot trencar:

public class Material {
    public int prestecsRegistrats;

    public void registrar()             { prestecsRegistrats++; }
    public void registrarDiversos(int n) {
        for (int i = 0; i < n; i++) {
            registrar();                     // detall d'implementacio
        }
    }
}

public class Dvd extends Material {
    public int vegadesRegistratDesDeDvd;

    @Override
    public void registrar() {
        vegadesRegistratDesDeDvd++;
        super.registrar();
    }

    @Override
    public void registrarDiversos(int n) {
        vegadesRegistratDesDeDvd += n;       // suma n...
        super.registrarDiversos(n);          // ...i super crida n vegades registrar()
    }
}

dvd.registrarDiversos(3) incrementa vegadesRegistratDesDeDvd en 6, no en 3: la superclasse crida internament un mètode que la subclasse ha sobreescrit. I si demà algú optimitza registrarDiversos per fer prestecsRegistrats += n sense cridar registrar(), el comptador de la subclasse passarà a valer 3. El mateix codi, resultat diferent, sense haver tocat la subclasse. Aquesta és la fragilitat: la subclasse depèn de detalls interns que ningú no va prometre mantenir.

La defensa: documentar exhaustivament quins mètodes crida internament la classe base, declarar final el que no s'hagi de tocar, i preferir composició quan no necessitis polimorfisme.

  1. BiblioTech: la jerarquia de materials prestables

Apliquem-ho tot al projecte. El model objectiu:

classDiagram
    class Material {
        +String titol
        +String referencia
        +boolean disponible
        +MULTA_MAXIMA double
        +LLINDAR_LLEU int
        +getDiesPrestec() int
        +getTarifaDiaria() double
        +calcularDiesRetard(int) int
        +calcularMulta(int) double
        +classificarGravetat(int) String
        +prestar() void
        +retornar() void
        +descriure() String
        +getTipus() String
    }
    class Llibre {
        +String autor
        +int anyPublicacio
        +getIsbn() String
    }
    class Revista {
        +int numero
        +String periodicitat
    }
    class Dvd {
        +int duracioMinuts
    }
    Material <|-- Llibre
    Material <|-- Revista
    Material <|-- Dvd

La classe base Material

package com.nexussoftware.bibliotech.domini;

/**
 * Material prestable del cataleg de Nexus Software.
 *
 * Defineix el que es comu a tots els suports: identificacio, disponibilitat i
 * el calcul de multes. Les subclasses nomes declaren en que es diferencien:
 * el termini de prestec i la tarifa diaria.
 */
public class Material {

    /** Termini estandard de prestec, en dies. El fan servir els llibres. */
    public static final int DIES_PRESTEC = 15;

    /** Tarifa estandard per dia de retard, en euros. */
    public static final double TARIFA_DIARIA = 0.25;

    /** Import maxim d'una multa, comu a tots els materials. */
    public static final double MULTA_MAXIMA = 20.0;

    /** Retard maxim, en dies, que es considera lleu. */
    public static final int LLINDAR_LLEU = 7;

    public static int materialsCreats = 0;

    public final String titol;
    public final String referencia;
    public boolean      disponible;

    public Material(String titol, String referencia, boolean disponible) {
        this.titol      = (titol == null || titol.isBlank()) ? "Sense titol" : titol.trim();
        this.referencia = (referencia == null || referencia.isBlank())
                          ? "000-0000000000" : referencia.trim();
        this.disponible = disponible;
        materialsCreats++;
    }

    // ---------- Parametres que cada suport redefineix ----------

    /** @return dies de termini d'aquest tipus de material. */
    public int getDiesPrestec() {
        return DIES_PRESTEC;
    }

    /** @return cost per dia de retard d'aquest tipus de material. */
    public double getTarifaDiaria() {
        return TARIFA_DIARIA;
    }

    /** @return nom llegible del suport, per a llistats. */
    public String getTipus() {
        return "Material";
    }

    // ---------- Regles comunes, escrites UNA sola vegada ----------

    /** @return dies de retard, saturats a zero, segons el termini d'aquest material. */
    public int calcularDiesRetard(int diesTranscorreguts) {
        return Math.max(0, diesTranscorreguts - getDiesPrestec());
    }

    /** @return multa en euros, amb el sostre comu aplicat. */
    public double calcularMulta(int diesTranscorreguts) {
        int retard = calcularDiesRetard(diesTranscorreguts);
        return Math.min(retard * getTarifaDiaria(), MULTA_MAXIMA);
    }

    /** @return "SENSE RETARD", "LLEU" o "GREU". */
    public String classificarGravetat(int diesTranscorreguts) {
        int retard = calcularDiesRetard(diesTranscorreguts);
        if (retard == 0)             return "SENSE RETARD";
        if (retard <= LLINDAR_LLEU)  return "LLEU";
        return "GREU";
    }

    public boolean estaDisponible() { return disponible; }

    public void prestar() {
        if (!disponible) {
            System.out.println("AVIS: '" + titol + "' ja estava prestat.");
            return;
        }
        disponible = false;
    }

    public void retornar() {
        if (disponible) {
            System.out.println("AVIS: '" + titol + "' ja estava disponible.");
            return;
        }
        disponible = true;
    }

    /** @return descripcio llegible; les subclasses l'amplien amb super.descriure(). */
    public String descriure() {
        return titol + " (ref. " + referencia + ")";
    }
}

Fixa't en la peça clau del disseny: calcularMulta no fa servir les constants directament, sinó que crida getDiesPrestec() i getTarifaDiaria(). Com que aquests mètodes es poden sobreescriure, la fórmula s'escriu una sola vegada i serveix per a tots els suports. És el mecanisme que la lliçó següent formalitzarà amb el nom de despatx dinàmic.

Les tres subclasses

package com.nexussoftware.bibliotech.domini;

/** Llibre tecnic del cataleg. Fa servir el termini i la tarifa estandard. */
public class Llibre extends Material {

    public final String autor;
    public final int    anyPublicacio;

    public Llibre(String titol, String autor, String isbn,
                  int anyPublicacio, boolean disponible) {
        super(titol, isbn, disponible);           // l'ISBN es la referencia del llibre
        this.autor = (autor == null || autor.isBlank()) ? "Desconegut" : autor.trim();

        if (anyPublicacio < 1450 || anyPublicacio > 2100) {
            System.out.println("AVIS: any invalid a '" + titol + "'. Es registra com a 0.");
            this.anyPublicacio = 0;
        } else {
            this.anyPublicacio = anyPublicacio;
        }
    }

    /** Alta habitual: un llibre nou entra disponible. */
    public Llibre(String titol, String autor, String isbn, int anyPublicacio) {
        this(titol, autor, isbn, anyPublicacio, true);
    }

    /** L'ISBN es la referencia d'un llibre; es mante el nom del domini. */
    public String getIsbn() { return referencia; }

    @Override
    public String getTipus() { return "Llibre"; }

    @Override
    public String descriure() {
        return super.descriure() + " - " + autor + ", " + anyPublicacio;
    }

    // No sobreescriu getDiesPrestec() ni getTarifaDiaria():
    // hereta els valors estandard de 15 dies i 0,25 EUR/dia.
}
package com.nexussoftware.bibliotech.domini;

/** Revista tecnica. Circula molt, aixi que el seu termini es mes curt i la tarifa menor. */
public class Revista extends Material {

    public static final int    DIES_PRESTEC_REVISTA  = 7;
    public static final double TARIFA_DIARIA_REVISTA = 0.10;

    public final int    numero;
    public final String periodicitat;

    public Revista(String titol, String referencia, int numero, String periodicitat) {
        super(titol, referencia, true);
        this.numero       = Math.max(0, numero);
        this.periodicitat = (periodicitat == null || periodicitat.isBlank())
                            ? "Desconeguda" : periodicitat.trim();
    }

    @Override
    public int getDiesPrestec() { return DIES_PRESTEC_REVISTA; }

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

    @Override
    public String getTipus() { return "Revista"; }

    @Override
    public String descriure() {
        return super.descriure() + " - n." + numero + " (" + periodicitat + ")";
    }
}
package com.nexussoftware.bibliotech.domini;

/** DVD de formacio. Poques copies: termini molt curt i tarifa doble. */
public class Dvd extends Material {

    public static final int    DIES_PRESTEC_DVD  = 3;
    public static final double TARIFA_DIARIA_DVD = 0.50;

    public final int duracioMinuts;

    public Dvd(String titol, String referencia, int duracioMinuts) {
        super(titol, referencia, true);
        this.duracioMinuts = Math.max(0, duracioMinuts);
    }

    @Override
    public int getDiesPrestec() { return DIES_PRESTEC_DVD; }

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

    @Override
    public String getTipus() { return "DVD"; }

    @Override
    public String descriure() {
        return super.descriure() + " - " + duracioMinuts + " min";
    }
}

Comparativa dels tres suports

Suport Termini Tarifa/dia Multa als 10 dies transcorreguts Camps propis
Llibre 15 dies 0,25 € 0,00 € (encara en termini) autor, anyPublicacio
Revista 7 dies 0,10 € 0,30 € (3 dies de retard) numero, periodicitat
Dvd 3 dies 0,50 € 3,50 € (7 dies de retard) duracioMinuts

Prova a BiblioTechApp

Llibre  javaEficac   = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Revista javaMagazine = new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral");
Dvd     cursSpring   = new Dvd("Curs de Spring", "DVD-0007", 240);

System.out.printf("%-9s %-22s %7s %8s %10s %10s%n",
                  "TIPUS", "TITOL", "TERMINI", "TARIFA", "MULTA 10d", "GRAVETAT");

imprimirFitxa(javaEficac);
imprimirFitxa(javaMagazine);
imprimirFitxa(cursSpring);

System.out.println();
System.out.println(javaEficac.descriure());
System.out.println(javaMagazine.descriure());
System.out.println(cursSpring.descriure());
private static void imprimirFitxa(Material m) {
    System.out.printf("%-9s %-22s %5d d %7.2f %9.2f  %s%n",
                      m.getTipus(), m.titol, m.getDiesPrestec(), m.getTarifaDiaria(),
                      m.calcularMulta(10), m.classificarGravetat(10));
}

Sortida:

TIPUS     TITOL                  TERMINI   TARIFA  MULTA 10d   GRAVETAT
Llibre    Java Eficac               15 d    0,25      0,00  SENSE RETARD
Revista   Java Magazine              7 d    0,10      0,30  LLEU
DVD       Curs de Spring             3 d    0,50      3,50  LLEU

Java Eficac (ref. 978-0000000001) - Joshua Bloch, 2018
Java Magazine (ref. REV-2024-42) - n.42 (Bimestral)
Curs de Spring (ref. DVD-0007) - 240 min

Observa una cosa que potser t'ha passat desapercebuda: el mètode imprimirFitxa rep un paràmetre de tipus Material i li passem un Llibre, una Revista i un Dvd. Funciona, i cadascun respon segons el seu propi tipus. Això és polimorfisme, el tema de la lliçó següent.

Nota sobre l'evolució del projecte. La classe Material tal com està escrita es pot instanciar (new Material(...)), i això no té sentit: a la biblioteca no hi ha "materials genèrics", hi ha llibres, revistes i DVD. A la lliçó 04-02 convertiràs Material en una classe abstracta, que impedeix crear instàncies soltes i permet declarar operacions sense cos que les subclasses estan obligades a implementar. Per ara, considera Material una bastida funcional.

Errors Habituals i Consells

  • Oblidar @Override. És l'error que produeix errors més difícils de trobar: un mètode mal escrit no sobreescriu res i la superclasse continua manant. Posa @Override sempre.
  • Creure que la subclasse pot accedir als camps private de la mare. Existeixen a l'objecte, però no són accessibles. Si la subclasse els necessita, la superclasse ha d'exposar un mètode (o declarar-los protected, amb compte; vegeu 03-07).
  • Esperar que els constructors s'heretin. No s'hereten. Cada subclasse declara els seus i crida super(...).
  • Fer servir herència per reutilitzar codi, sense relació "és un". El cas Prestec extends Llibre de l'apartat 11. Si només vols reutilitzar, compon.
  • Cridar mètodes sobreescrivibles des del constructor. El camp encara val null. Documentat a l'apartat 6.
  • Canviar la visibilitat a menys permissiva en sobreescriure. publicprotected no compila: trencaria el contracte de la superclasse.
  • Jerarquies de cinc o sis nivells. Costa més entendre-les que el problema que resolen.
  • Consell: digues la frase en veu alta. "Un DVD és un material prestable": sona bé, herència. "Un préstec és un llibre": sona malament, composició.
  • Consell: puja a la superclasse només el que sigui comú a totes les subclasses. Si un camp el fan servir dues de tres, no pertany a la classe base.
  • Consell: fes servir la vista de jerarquia de l'IDE (Ctrl+H a IntelliJ) per veure d'un cop d'ull qui estén qui i quins mètodes se sobreescriuen.

Exercicis

Exercici 1: afegir un suport nou

Nexus Software incorpora audiollibres al catàleg: es presten 10 dies, amb una tarifa de 0,15 €/dia, i tenen un camp propi narrador i un altre duracioMinuts. Crea la classe AudioLlibre com a subclasse de Material, sobreescrivint el que calgui i sense tocar ni una línia de Material, Llibre, Revista o Dvd. Afegeix-la a la taula comparativa de BiblioTechApp i comprova que la multa als 20 dies transcorreguts és d'1,50 €.

Exercici 2: traçar l'ordre de construcció

Escriu una jerarquia de tres nivells (MaterialLlibreLlibreSignat) on cada classe tingui un bloc d'inicialització d'instància i un constructor, tots dos amb un System.out.println identificatiu. Predigues la sortida de new LlibreSignat(...) abans d'executar-la i després verifica-la. A continuació, provoca deliberadament el problema de l'apartat 6: fes que el constructor de Material cridi un mètode que LlibreSignat sobreescrigui fent servir un dels seus camps, i explica el resultat.

Exercici 3: detectar herència mal aplicada

Un company proposa aquestes quatre jerarquies per a BiblioTech. Per a cadascuna, decideix si l'herència és adequada i, si no ho és, proposa l'alternativa correcta amb composició:

class Empleat extends Material { }
class Bibliotecari extends Empleat { }
class Cataleg extends Llibre { }
class LlibreDescatalogat extends Llibre { }

Solucions

Solució 1

package com.nexussoftware.bibliotech.domini;

/** Audiollibre del cataleg. Termini intermedi i tarifa reduida. */
public class AudioLlibre extends Material {

    public static final int    DIES_PRESTEC_AUDIO  = 10;
    public static final double TARIFA_DIARIA_AUDIO = 0.15;

    public final String narrador;
    public final int    duracioMinuts;

    public AudioLlibre(String titol, String referencia,
                       String narrador, int duracioMinuts) {
        super(titol, referencia, true);
        this.narrador = (narrador == null || narrador.isBlank())
                        ? "Desconegut" : narrador.trim();
        this.duracioMinuts = Math.max(0, duracioMinuts);
    }

    @Override
    public int getDiesPrestec() { return DIES_PRESTEC_AUDIO; }

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

    @Override
    public String getTipus() { return "Audiollibre"; }

    @Override
    public String descriure() {
        return super.descriure() + " - narrat per " + narrador
               + ", " + duracioMinuts + " min";
    }
}

Ús:

AudioLlibre audio = new AudioLlibre("Refactoritzacio", "AUD-0003", "Nuria Vidal", 610);
imprimirFitxa(audio);
System.out.println(audio.descriure());
System.out.printf("Multa a 20 dies: %.2f EUR (%s)%n",
                  audio.calcularMulta(20), audio.classificarGravetat(20));

Sortida:

Audiollibre Refactoritzacio           10 d    0,15      0,00  SENSE RETARD
Refactoritzacio (ref. AUD-0003) - narrat per Nuria Vidal, 610 min
Multa a 20 dies: 1,50 EUR (GREU)

Comprova els dos càlculs a mà, perquè són la millor manera de verificar que l'herència funciona com esperes. Amb 10 dies transcorreguts (els que fa servir imprimirFitxa), el termini de l'audiollibre és de 10 dies, així que el retard és 0: no hi ha multa i la gravetat és SENSE RETARD. Amb 20 dies transcorreguts, el retard és 20 − 10 = 10 dies; 10 × 0,15 = 1,50 €, molt per sota del sostre de 20 €; i com que 10 supera el LLINDAR_LLEU de 7 dies, la gravetat és GREU.

El més important de l'exercici: per afegir un suport nou has escrit una classe i zero modificacions al codi existent. imprimirFitxa funciona amb audiollibres sense haver-se assabentat que existeixen. Aquesta propietat —estendre sense modificar— és una de les raons per les quals la POO es va imposar al programari de gran escala, i la lliçó següent l'explota a fons.

Solució 2

package com.nexussoftware.bibliotech;

class MaterialTraca {
    { System.out.println("1. bloc de Material"); }

    MaterialTraca() {
        System.out.println("2. constructor de Material");
        System.out.println("   descriure() -> " + descriure());   // PERILL
    }

    String descriure() { return "material generic"; }
}

class LlibreTraca extends MaterialTraca {
    { System.out.println("3. bloc de Llibre"); }

    LlibreTraca() {
        super();
        System.out.println("4. constructor de Llibre");
    }
}

class LlibreSignatTraca extends LlibreTraca {
    private final String signant;

    { System.out.println("5. bloc de LlibreSignat"); }

    LlibreSignatTraca(String signant) {
        super();
        this.signant = signant;
        System.out.println("6. constructor de LlibreSignat");
    }

    @Override
    String descriure() { return "llibre signat per " + signant; }
}
new LlibreSignatTraca("Martin Fowler");

Sortida:

1. bloc de Material
2. constructor de Material
   descriure() -> llibre signat per null
3. bloc de Llibre
4. constructor de Llibre
5. bloc de LlibreSignat
6. constructor de LlibreSignat

Explicació. La construcció va de l'arrel a la fulla: els blocs i el constructor de cada nivell s'executen sencers abans de passar al següent. Per això l'ordre és 1-2, 3-4, 5-6.

El problema de l'apartat 6 es veu a la tercera línia. El constructor de MaterialTraca crida descriure(), i com que l'objecte real és un LlibreSignatTraca, la JVM executa la versió sobreescrita (això és el despatx dinàmic de la lliçó següent). Però en aquell instant el camp signant encara no s'ha assignat —la seva assignació és al pas 6, quatre passos més tard—, així que val null i la sortida és llibre signat per null. I observa el detall inquietant: signant és final, un camp que "no pot canviar", i tanmateix l'hem observat amb dos valors diferents durant la vida de l'objecte.

Solució 3

class Empleat extends Material — INCORRECTA. "Un empleat és un material prestable" és fals i, a més, moralment qüestionable. Empleat heretaria prestar(), retornar() i calcularMulta(), tots sense sentit, i podria acabar en un catàleg de materials. Alternativa: cap relació. Empleat és una classe independent que apareix associada des de Prestec.

class Bibliotecari extends Empleat — CORRECTA (amb matisos). "Un bibliotecari és un empleat" és cert, i un bibliotecari tindria comportament propi (donar d'alta materials, cancel·lar multes) a més de l'heretat. És un ús legítim. El matís: si l'única diferència fos un camp String rol, l'herència seria excessiva; n'hi hauria prou amb un atribut. Introdueix la subclasse només quan aporti comportament diferent, no només una dada.

class Cataleg extends Llibre — INCORRECTA, i de les pitjors. Un catàleg no és un llibre: un catàleg conté llibres. Aquí es confon el contenidor amb el contingut, un error clàssic. Cataleg heretaria prestar(), getIsbn() i autor, absurds per a una col·lecció. Alternativa per composició, que és exactament el que faràs al mòdul 5:

public class Cataleg {
    // Conte materials; l'emmagatzematge multiple arriba al modul 5.
    // De moment seria un array; despres, un ArrayList o un HashMap.
}

class LlibreDescatalogat extends Llibre — DUBTOSA. La frase "un llibre descatalogat és un llibre" és certa, així que l'herència no és absurda. Però pregunta't quin comportament canvia: probablement només que no es pot prestar. Això és un estat, no un tipus:

public class Llibre extends Material {
    private boolean descatalogat;

    @Override
    public void prestar() {
        if (descatalogat) {
            System.out.println("AVIS: '" + titol + "' esta descatalogat.");
            return;
        }
        super.prestar();
    }
}

El senyal d'alarma general: si un objecte pot passar d'una subclasse a una altra durant la seva vida —un llibre es descataloga i després es torna a catalogar—, l'herència és el mecanisme equivocat, perquè en Java un objecte no pot canviar de classe un cop creat. Els estats que canvien es modelen amb camps.

Conclusió

Has après el mecanisme amb què Java expressa l'especialització. Saps quan escau —només quan la frase "X és un Y" és certa sempre— i com s'escriu amb extends. Coneixes amb precisió què s'hereta i què no: els camps privats existeixen però no són accessibles, i els constructors no s'hereten mai. Domines super(...) per encadenar constructors i super.metode() per estendre comportament sense duplicar-lo, i has traçat l'ordre de construcció d'una jerarquia completa, inclòs el perill real de cridar mètodes sobreescrivibles des d'un constructor i trobar-te camps final valent null. Tens clara —i avalada per una taula— la diferència entre sobreescriure i sobrecarregar, i saps que @Override no és decoració, sinó la xarxa de seguretat que converteix un error tipogràfic silenciós en un error de compilació. Saps fer servir final per prohibir l'extensió, coneixes l'arrel universal Object i, sobretot, tens criteri per preferir composició a herència i per reconèixer les jerarquies profundes i la fragilitat de la classe base abans de patir-les.

BiblioTech ha canviat d'escala: ja no gestiona llibres, gestiona materials prestables. Material concentra la identificació, la disponibilitat i les regles de multa escrites una sola vegada, i Llibre, Revista i Dvd declaren únicament en què es diferencien: el seu termini i la seva tarifa. Afegir un audiollibre t'ha costat una classe nova i zero canvis al codi existent.

I ha aparegut una cosa que encara no té nom. imprimirFitxa(Material m) rep indistintament un llibre, una revista o un DVD, i cadascun respon segons el que realment és. calcularMulta està escrit una sola vegada, a Material, i tanmateix cobra 0,25 € al llibre i 0,50 € al DVD. Aquest mecanisme s'anomena polimorfisme, és el pilar que fa que l'herència valgui la pena, i és el tema de la lliçó següent: veuràs com la JVM decideix en temps d'execució quin mètode executar, què són l'upcasting i el downcasting, com instanceof amb patrons de Java modern substitueix els antics if sobre el tipus, i per què aquests if eren, des del principi, una olor de disseny.

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