En tancar la lliçó anterior va quedar una escletxa oberta a BiblioTech: Material continua sent una classe instanciable. Res no impedeix escriure new Material("Alguna cosa", "REF-1", true) i obtenir un objecte que no és un llibre, ni una revista, ni un DVD; un objecte sense autor, sense número i sense durada, que respon "Material" quan li preguntes el seu tipus. És un objecte que no hauria d'existir, i la seva existència és un accident del disseny, no una decisió.

La classe abstracta tanca aquella porta. És una classe que declara explícitament que està incompleta: serveix com a base comuna —amb camps, constructor i codi compartit— però no es pot instanciar, i pot obligar les seves subclasses a implementar els mètodes que només elles poden decidir. Allà on la interfície diu "això és el que cal saber fer", la classe abstracta diu "això és el que ja està fet, i això és el que et toca a tu". En acabar aquesta lliçó, Material serà abstracta, calcularMulta estarà escrit una sola vegada per a tot el sistema mitjançant el patró Template Method, i sabràs triar amb criteri entre interfície, classe abstracta o —el més freqüent en el disseny professional— totes dues alhora.

Contingut

  1. abstract en classes: què significa i què impedeix
  2. Mètodes abstractes: l'obligació que s'hereta
  3. Una classe abstracta AMB estat: camps, constructor i mètodes concrets
  4. Per a què serveix el constructor d'una classe que no s'instancia?
  5. La subclasse incompleta també ha de ser abstracta
  6. Interfície enfront de classe abstracta: la taula completa
  7. Regla pràctica de decisió
  8. El patró Template Method
  9. Combinar-les totes dues: AbstractXxx
  10. BiblioTech: Material esdevé abstracta
  11. Refactorització de Llibre, Revista i Dvd
  12. Errors Habituals i Consells
  13. Exercicis

  1. abstract en classes: què significa i què impedeix

El modificador abstract aplicat a una classe declara que aquella classe és conceptualment incompleta: representa una idea general de la qual no existeixen exemplars concrets.

public abstract class Material {
    // ... exactament el mateix que abans ...
}

Amb aquesta sola paraula, això deixa de compilar:

Material m = new Material("Alguna cosa", "REF-1", true);
error: Material is abstract; cannot be instantiated

És important entendre què es prohibeix i què no:

Operació Permesa en una classe abstracta?
new Material(...) No. Error de compilació
Material m = new Llibre(...) Sí. És un tipus vàlid per declarar
Material[] cataleg = new Material[10] Sí. L'array desa referències, no instàncies
Tenir camps, fins i tot private
Tenir constructor , i és fonamental (apartat 4)
Tenir mètodes concrets amb cos
Tenir mètodes abstract sense cos Sí, i és el que la caracteritza
Tenir mètodes static i final
Estendre una altra classe
Implementar interfícies Sí, i és molt habitual (apartat 9)
No tenir cap mètode abstracte Sí, és legal (encara que poc comú)

Aquest últim punt sorprèn: una classe pot ser abstract sense tenir ni un sol mètode abstracte. És perfectament vàlid i de vegades útil: la marques com a abstracta simplement per declarar que instanciar-la no té sentit, encara que tècnicament estigui completa.

El guany immediat és de modelatge. "Material" és una abstracció: a la biblioteca de Nexus Software hi ha llibres, revistes i DVD, però no hi ha "materials" a seques. El codi passa a dir la veritat sobre el domini, i el compilador la fa complir.

  1. Mètodes abstractes: l'obligació que s'hereta

Un mètode abstracte és un mètode declarat sense cos, acabat en punt i coma, que la classe promet que existirà però es nega a implementar:

public abstract class Material {

    /** Cada suport declara la seva etiqueta llegible. */
    public abstract String getTipus();

    /** Cada suport declara el seu termini de prestec en dies. */
    public abstract int getDiesPrestec();

    /** Cada suport declara la seva tarifa diaria de multa. */
    public abstract double getTarifaDiaria();
}

Les regles són estrictes i val la pena tenir-les totes juntes:

  • Un mètode abstracte només pot existir dins d'una classe abstracta (o d'una interfície). Si poses un mètode abstracte en una classe normal, l'error és missing method body, or declare abstract.
  • Tota subclasse concreta els ha d'implementar tots. Si en falta un, no compila.
  • abstract és incompatible amb private, static i final. Amb private perquè la subclasse no el veuria; amb static perquè els mètodes estàtics no són polimòrfics (03-06); amb final perquè final prohibeix exactament el que abstract exigeix.

La diferència amb el disseny anterior és de garanties. Compara les dues versions de getDiesPrestec:

// ABANS (modul 3): implementacio de farciment
public int getDiesPrestec() { return DIES_PRESTEC; }   // 15 dies "per si de cas"

// ARA: obligacio
public abstract int getDiesPrestec();

Amb la primera, si demà afegeixes AudioLlibre extends Material i te'n descuides el termini, el programa compila i aplica silenciosament 15 dies. L'error no es detecta fins que un empleat es queixa d'una multa mal calculada. Amb la segona, no compila: l'error arriba al segon zero, amb el nom del mètode i el número de línia.

Aquesta és la diferència entre un valor per defecte i un contracte. El valor per defecte amaga l'oblit; el contracte el denuncia.

  1. Una classe abstracta AMB estat: camps, constructor i mètodes concrets

Aquí hi ha la diferència essencial amb una interfície: una classe abstracta pot desar dades. I per tant pot escriure codi compartit de debò, no només codi que s'apuntala en crides al contracte.

public abstract class Material {

    // 1. Constants compartides
    public static final double MULTA_MAXIMA = 20.0;
    public static final int    LLINDAR_LLEU = 7;

    // 2. Estat d'instancia: IMPOSSIBLE en una interficie
    private final String titol;
    private final String referencia;
    private boolean      disponible;

    // 3. Constructor: IMPOSSIBLE en una interficie
    protected 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;
    }

    // 4. Metodes concrets que operen sobre aquest estat
    public String  getTitol()       { return titol; }
    public String  getReferencia()  { return referencia; }
    public boolean estaDisponible() { return disponible; }

    public boolean prestar() {
        if (!disponible) { return false; }
        disponible = false;
        return true;
    }

    // 5. Metodes abstractes que les subclasses han d'omplir
    public abstract String getTipus();
    public abstract int    getDiesPrestec();
    public abstract double getTarifaDiaria();
}

Les cinc seccions numerades resumeixen el repartiment: la classe abstracta aporta el que és comú (2, 3, 4), i delega el que varia (5). Cap interfície no pot oferir els punts 2 i 3.

És la resposta directa a una pregunta que potser et vas fer a 04-01: si un mètode default pot tenir cos, per a què necessito una classe abstracta? Perquè prestar() necessita el camp disponible. Un default no el pot tenir: hauria de cridar estaDisponible() i un hipotètic setDisponible(), ampliant el contracte amb un mutador que trenca l'encapsulament que tant vas cuidar a 03-07. La classe abstracta desa el camp, el manté private i exposa només les operacions de negoci.

  1. Per a què serveix el constructor d'una classe abstracta?

És el dubte clàssic: si new Material(...) està prohibit, quin sentit té aquell constructor?

La resposta és a la cadena d'inicialització que vas estudiar a 03-04: el constructor d'una subclasse sempre invoca primer el de la seva superclasse, explícitament amb super(...) o implícitament. Aquell constructor sí que s'executa, simplement mai tot sol.

public class Llibre extends Material {
    public Llibre(String titol, String autor, String isbn, int any) {
        super(titol, isbn, true);        // executa el constructor de Material
        this.autor = autor;
        this.anyPublicacio = any;
    }
}
flowchart TD
    A["new Llibre(...)"] --> B["es reserva memoria per a l'objecte complet"]
    B --> C["constructor de Llibre: super(titol, isbn, true)"]
    C --> D["constructor d'Object"]
    D --> E["cos del constructor de Material: valida i inicialitza titol, referencia, disponible"]
    E --> F["cos del constructor de Llibre: inicialitza autor i anyPublicacio"]
    F --> G["objecte Llibre completament construit"]

Així que el constructor d'una classe abstracta té tres funcions molt concretes:

  1. Inicialitzar l'estat comú que la subclasse no ha de tocar (els camps són private).
  2. Centralitzar la validació. La comprovació de títol i referència s'escriu una vegada i l'apliquen Llibre, Revista i Dvd, sense possibilitat de saltar-se-la.
  3. Garantir els invariants de la part comuna des del primer instant de vida de l'objecte.

I hi ha una decisió de disseny a la signatura: declarar-lo protected en lloc de public. En una classe abstracta, public al constructor és enganyós —suggereix que algú el pot cridar des de fora, i no pot—, mentre que protected diu exactament la veritat: aquest constructor existeix per a les subclasses. És el conveni que segueix el mateix JDK.

  1. La subclasse incompleta també ha de ser abstracta

Què passa si una subclasse implementa només una part dels mètodes abstractes? Que continua sent incompleta, i Java t'obliga a dir-ho:

/** Base comuna dels suports que es consulten a sala i no surten de l'edifici. */
public abstract class MaterialDeConsulta extends Material {

    protected MaterialDeConsulta(String titol, String referencia) {
        super(titol, referencia, true);
    }

    /** Tots els materials de consulta comparteixen termini i tarifa... */
    @Override public int    getDiesPrestec()  { return 1; }
    @Override public double getTarifaDiaria() { return 1.0; }

    // ... pero getTipus() segueix sense implementar: la classe segueix sent abstracta
}

Si li lleves l'abstract a MaterialDeConsulta, el compilador diria:

error: MaterialDeConsulta is not abstract and does not override
       abstract method getTipus() in Material

L'obligació s'hereta cap avall fins que algú la compleix. Això permet jerarquies de diversos nivells on cada nivell resol el que sap i deixa la resta pendent:

classDiagram
    class Material {
        <<abstract>>
        +getTipus()* String
        +getDiesPrestec()* int
        +getTarifaDiaria()* double
    }
    class MaterialDeConsulta {
        <<abstract>>
        +getDiesPrestec() int
        +getTarifaDiaria() double
    }
    class Atlas {
        +getTipus() String
    }
    Material <|-- MaterialDeConsulta
    MaterialDeConsulta <|-- Atlas

Atlas és la primera classe concreta de la branca: implementa l'única cosa que quedava pendent i ja es pot instanciar.

  1. Interfície enfront de classe abstracta: la taula completa

Aquesta és la taula que es va anunciar a 04-01. Estudia-la sencera: cobreix les sis dimensions que realment decideixen.

Dimensió Interfície Classe abstracta
Estat d'instància Impossible. Només public static final Camps de qualsevol tipus i visibilitat
Constructor No en té Sí, i s'executa via super(...)
Herència múltiple Una classe n'implementa diverses Una classe n'estén una de sola
Visibilitat dels membres Tot public (llevat dels private de Java 9, invisibles a fora) public, protected, package, private
Mètodes amb cos Sí: default, static, private Sí, sense restriccions
Blocs d'inicialització No Sí, estàtics i d'instància
Membres protected No Sí: el mecanisme natural per a les subclasses
Evolució de l'API Afegir un abstracte trenca; afegir un default no Afegir un mètode concret no trenca; un d'abstracte sí
Relació que expressa "pot fer" (capacitat) "és un" (identitat)
Acoblament que imposa Mínim: no consumeix l'herència Alt: consumeix l'única herència disponible
Exemple del JDK Comparable, Runnable, List AbstractList, InputStream, Number

Tres files mereixen comentari addicional:

Evolució de l'API. És asimètrica i sovint es confon. En una interfície, afegir un mètode abstracte trenca tots els implementadors, però un default no trenca ningú. En una classe abstracta, afegir un mètode concret és segur (totes les subclasses l'hereten), però afegir-ne un d'abstracte trenca totes les subclasses concretes. Cadascuna té la seva via segura de créixer.

Acoblament. És l'argument decisiu i sovint el que s'oblida. Si obligues els teus usuaris a estendre la teva classe abstracta, els gastes la seva única herència i no podran estendre res més. Una interfície no costa res en aquest sentit. Per això la recomanació professional és: el tipus públic és la interfície; la classe abstracta és una ajuda opcional.

Membres protected. Una classe abstracta pot oferir a les seves subclasses eines que la resta del món no veu. Una interfície no: tot el que declara és públic. Quan necessites aquest "canal privat" cap a les subclasses, la classe abstracta és l'única opció.

  1. Regla pràctica de decisió

flowchart TD
    A["Necessito un tipus comu"] --> B{"Cal estat compartit o un constructor?"}
    B -- "No" --> C{"El signaran classes sense parentiu?"}
    B -- "Si" --> D["Classe abstracta"]
    C -- "Si" --> E["Interficie"]
    C -- "No" --> F{"Necessito codi compartit?"}
    F -- "No" --> E
    F -- "Si" --> G["Interficie mes classe abstracta base"]
    D --> H{"Tambe vull que el signin altres?"}
    H -- "Si" --> G
    H -- "No" --> D

I en tres frases memoritzables:

  • Interfície quan defineixes què es pot fer i vols que ho signin classes que no comparteixen família. És l'opció per defecte: comença sempre per aquí.
  • Classe abstracta quan comparteixes estat, constructor i codi real entre classes que sí que formen una família genuïna.
  • Totes dues quan vols les dues coses: la interfície com a tipus públic i la classe abstracta com a implementació base opcional. És el que fa el JDK i el que faràs a BiblioTech.

  1. El patró Template Method

Ara arriba un dels usos més potents de les classes abstractes. Observa aquest mètode, que ja existeix al teu Material des del mòdul 3:

public double calcularMulta(int diesTranscorreguts) {
    return Math.min(calcularDiesRetard(diesTranscorreguts) * getTarifaDiaria(),
                    MULTA_MAXIMA);
}

L'interessant és la seva estructura: defineix l'esquelet invariable de l'algorisme —calcular el retard, multiplicar-lo per la tarifa, aplicar el sostre— però delega en getTarifaDiaria(), que cada subclasse implementa a la seva manera. L'esquelet s'escriu una vegada; els passos variables, tantes vegades com suports hi hagi.

Això és exactament el patró Template Method: un mètode concret de la classe base defineix la seqüència de passos, i els passos que varien són mètodes abstractes que les subclasses omplen.

I hi ha un detall clau que fa el patró sòlid: el mètode plantilla es declara final.

/**
 * PLANTILLA: la sequencia del calcul es politica de l'empresa
 * i cap subclasse no la pot alterar.
 */
public final double calcularMulta(int diesTranscorreguts) {
    int retard = calcularDiesRetard(diesTranscorreguts);   // pas fix
    double bruta = retard * getTarifaDiaria();             // PAS VARIABLE
    return Math.min(bruta, MULTA_MAXIMA);                  // pas fix (sostre)
}

Per què final? Perquè el sostre de 20 € i la fórmula són política de Nexus Software, no una decisió de cada suport. Sense final, un Dvd podria sobreescriure calcularMulta i saltar-se el sostre. Amb final, el compilador ho impedeix: les subclasses només poden influir a través dels forats que la plantilla els ofereix.

flowchart TD
    A["calcularMulta(dies) FINAL a Material"] --> B["pas fix: calcularDiesRetard(dies)"]
    B --> C["PAS VARIABLE: getTarifaDiaria()"]
    C --> D["pas fix: aplicar MULTA_MAXIMA"]
    D --> E["resultat"]
    C -.-> F["Llibre retorna 0.25"]
    C -.-> G["Revista retorna 0.10"]
    C -.-> H["Dvd retorna 0.50"]

Repartir així les responsabilitats té un nom a la literatura: el principi de Hollywood — "no ens truquis, ja et trucarem nosaltres". La subclasse no controla el flux; es limita a omplir els forats que la classe base li reserva.

Aquest patró es diu així formalment perquè és un dels patrons de disseny clàssics. Aquí l'has descobert de manera natural, escrivint la classe que calia; a la lliçó 12-02 el veuràs formalitzat al costat de Strategy, Factory, Observer i els altres, amb la seva fitxa completa i les seves alternatives.

  1. Combinar-les totes dues: AbstractXxx

Gairebé mai tries entre interfície i classe abstracta: fas servir totes dues en capes.

  • La interfície és el tipus públic. El que declaren les variables, els paràmetres i els retorns.
  • La classe abstracta implementa la interfície i ofereix una base parcial amb la feina repetitiva ja resolta.
  • Les classes concretes estenen la base i només escriuen el que és seu.

I qui no vulgui la base, implementa la interfície directament. L'ajuda és opcional, i tota la gràcia és aquí.

El JDK està construït així, i el conveni de noms el delata:

Interfície (tipus públic) Base parcial (ajuda opcional) Classe concreta
List AbstractList ArrayList, LinkedList
Map AbstractMap HashMap, TreeMap
Set AbstractSet HashSet

ArrayList és un AbstractList i és una List. Però pots escriure la teva pròpia List sense tocar AbstractList. Totes aquestes classes arriben al mòdul 5; el que importa avui és reconèixer el patró estructural.

A BiblioTech el repartiment queda exactament igual:

classDiagram
    class Prestable {
        <<interface>>
        +prestar() boolean
        +retornar() boolean
        +estaDisponible() boolean
        +getDiesPrestec() int
    }
    class Notificable {
        <<interface>>
        +getCanalAvis() String
        +generarAvis(int) String
    }
    class Material {
        <<abstract>>
        -titol String
        -referencia String
        -disponible boolean
        +calcularMulta(int)$ double
        +getTipus()* String
    }
    class SalaReunions
    Prestable <|.. Material
    Notificable <|.. Material
    Prestable <|.. SalaReunions
    Material <|-- Llibre
    Material <|-- Revista
    Material <|-- Dvd

SalaReunions implementa Prestable sense passar per Material: no vol la base, i no la necessita. És la prova que l'ajuda és opcional.

  1. BiblioTech: Material esdevé abstracta

Aquest és l'estat final de la classe després d'aplicar tot el que s'ha après.

package com.nexussoftware.bibliotech.domini;

/**
 * Base abstracta de tot material del cataleg de Nexus Software.
 *
 * <p>Defineix l'estat comu (titol, referencia, disponibilitat), les regles
 * de negoci compartides i la plantilla del calcul de multes. Cada suport
 * concret aporta el seu tipus, el seu termini i la seva tarifa.</p>
 */
public abstract class Material implements Prestable, Notificable {

    public static final double MULTA_MAXIMA = 20.0;
    public static final int    LLINDAR_LLEU = 7;

    private static int materialsCreats = 0;

    private final String titol;
    private final String referencia;
    private boolean      disponible;

    /** Constructor protected: existeix per a les subclasses, no per a l'exterior. */
    protected 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++;
    }

    // ---------- El que CADA suport ha de declarar ----------

    /** @return etiqueta llegible del suport: "Llibre", "Revista", "DVD". */
    public abstract String getTipus();

    /** @return dies de termini de prestec d'aquest suport. */
    @Override public abstract int getDiesPrestec();

    /** @return euros de multa per dia de retard d'aquest suport. */
    public abstract double getTarifaDiaria();

    // ---------- Estat comu (implementat una sola vegada) ----------

    public String  getTitol()       { return titol; }
    public String  getReferencia()  { return referencia; }
    @Override public boolean estaDisponible() { return disponible; }

    public static int getMaterialsCreats() { return materialsCreats; }

    // ---------- Regles de negoci: PLANTILLES final ----------

    /** Dies de retard sobre el termini del suport. Mai negatiu. */
    public final int calcularDiesRetard(int diesTranscorreguts) {
        return Math.max(0, diesTranscorreguts - getDiesPrestec());
    }

    /**
     * TEMPLATE METHOD. L'esquelet del calcul es politica d'empresa
     * i per aixo es final; el pas variable es getTarifaDiaria().
     */
    public final double calcularMulta(int diesTranscorreguts) {
        int    retard = calcularDiesRetard(diesTranscorreguts);
        double bruta  = retard * getTarifaDiaria();
        return Math.min(bruta, MULTA_MAXIMA);
    }

    /** Classificacio del retard. A 04-07 deixara de retornar String. */
    public final String classificarGravetat(int diesTranscorreguts) {
        int retard = calcularDiesRetard(diesTranscorreguts);
        if (retard == 0)            { return "SENSE RETARD"; }
        if (retard <= LLINDAR_LLEU) { return "LLEU"; }
        return "GREU";
    }

    // ---------- Operacions de negoci (contracte Prestable) ----------

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

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

    // ---------- Contracte Notificable ----------

    @Override public String getCanalAvis() { return "correu"; }

    public int getDiesPreavis() { return 2; }

    @Override
    public String generarAvis(int diesTranscorreguts) {
        return String.format("Avis per %s amb %d dies de preavis: '%s' acumula %.2f EUR.",
                             getCanalAvis(), getDiesPreavis(), titol,
                             calcularMulta(diesTranscorreguts));
    }

    // ---------- Representacio i identitat ----------

    public String descriure() {
        return getTipus() + " \"" + titol + "\" (ref. " + referencia + ")";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        Material altre = (Material) o;
        return referencia.equals(altre.referencia);
    }

    @Override
    public int hashCode() { return referencia.hashCode(); }

    @Override
    public String toString() {
        return getTipus() + "[ref=" + referencia + ", titol=" + titol
             + ", disponible=" + disponible + "]";
    }
}

Quatre canvis que mereixen atenció:

  1. abstract class: new Material(...) ja no compila.
  2. Constructor protected: diu la veritat sobre qui el pot cridar.
  3. getTipus(), getDiesPrestec() i getTarifaDiaria() són abstractes: desapareixen les constants de farciment DIES_PRESTEC = 15 i TARIFA_DIARIA = 0.25 de Material. Cada suport declara les seves i ningú no se les pot descuidar.
  4. calcularDiesRetard, calcularMulta i classificarGravetat són final: són la política de l'empresa, no negociable per subclasse.

I observa descriure(): ara crida getTipus(), un mètode abstracte. Un mètode concret de la classe base invocant un mètode que encara no existeix. Funciona gràcies al despatx dinàmic de 03-06: en temps d'execució, this és sempre un objecte concret, i el seu getTipus() està implementat.

  1. Refactorització de Llibre, Revista i Dvd

Ara cada suport declara les seves tres decisions, i cap no es pot ometre.

package com.nexussoftware.bibliotech.domini;

/** Llibre tecnic del cataleg. Termini llarg i tarifa estandard. */
public class Llibre extends Material {

    public static final int    DIES_PRESTEC_LLIBRE  = 15;
    public static final double TARIFA_DIARIA_LLIBRE = 0.25;

    private final String autor;
    private final int    anyPublicacio;

    public Llibre(String titol, String autor, String isbn,
                  int anyPublicacio, boolean disponible) {
        super(titol, isbn, disponible);
        this.autor = (autor == null || autor.isBlank()) ? "Desconegut" : autor.trim();
        this.anyPublicacio = (anyPublicacio < 1450 || anyPublicacio > 2100)
                             ? 0 : anyPublicacio;
    }

    public Llibre(String titol, String autor, String isbn, int anyPublicacio) {
        this(titol, autor, isbn, anyPublicacio, true);
    }

    public String getAutor()         { return autor; }
    public int    getAnyPublicacio() { return anyPublicacio; }
    public String getIsbn()          { return getReferencia(); }

    // --- Les tres decisions OBLIGATORIES ---
    @Override public String getTipus()        { return "Llibre"; }
    @Override public int    getDiesPrestec()  { return DIES_PRESTEC_LLIBRE; }
    @Override public double getTarifaDiaria() { return TARIFA_DIARIA_LLIBRE; }

    @Override
    public String descriure() {
        return super.descriure() + " - " + autor + ", " + anyPublicacio;
    }
}
package com.nexussoftware.bibliotech.domini;

/** Revista tecnica. Circula molt: termini curt i tarifa baixa. */
public class Revista extends Material {

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

    private final int    numero;
    private 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();
    }

    public int    getNumero()       { return numero; }
    public String getPeriodicitat() { return periodicitat; }

    @Override public String getTipus()        { return "Revista"; }
    @Override public int    getDiesPrestec()  { return DIES_PRESTEC_REVISTA; }
    @Override public double getTarifaDiaria() { return TARIFA_DIARIA_REVISTA; }

    @Override public String getCanalAvis()    { return "xat"; }
    @Override public int    getDiesPreavis()  { return 1; }

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

/** DVD de formacio. Poques copies: termini minim 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;

    private final int duracioMinuts;

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

    public int getDuracioMinuts() { return duracioMinuts; }

    @Override public String getTipus()        { return "DVD"; }
    @Override public int    getDiesPrestec()  { return DIES_PRESTEC_DVD; }
    @Override public double getTarifaDiaria() { return TARIFA_DIARIA_DVD; }

    @Override public String getCanalAvis()    { return "telefon"; }
    @Override public int    getDiesPreavis()  { return 1; }

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

Prova del resultat:

Material[] cataleg = {
    new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
    new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
    new Dvd("Refactoritzacio en directe", "DVD-0007", 95)
};

System.out.printf("%-10s %-8s %-8s %-10s %s%n",
                  "TIPUS", "TERMINI", "TARIFA", "MULTA@20d", "GRAVETAT");
for (Material m : cataleg) {
    System.out.printf("%-10s %-8d %-8.2f %-10.2f %s%n",
                      m.getTipus(), m.getDiesPrestec(), m.getTarifaDiaria(),
                      m.calcularMulta(20), m.classificarGravetat(20));
}

// Material generic = new Material("X", "REF", true);   // NO COMPILA
TIPUS      TERMINI  TARIFA   MULTA@20d  GRAVETAT
Llibre     15       0,25     1,25       LLEU
Revista    7        0,10     1,30       GREU
DVD        3        0,50     8,50       GREU

Un únic calcularMulta, escrit una sola vegada, produint tres resultats diferents i correctes. I la línia comentada és la millora principal: l'objecte sense sentit ja no es pot crear.

Errors Habituals i Consells

Posar un mètode abstracte en una classe no abstracta. L'error és missing method body, or declare abstract. Si un mètode no es pot implementar a la classe base, la classe base és abstracta. No hi ha terme mitjà.

Intentar new sobre la classe abstracta. Material m = new Material(...) no compila. El que sí que és legal, i confon molta gent, és Material m = new Llibre(...): la classe abstracta és un tipus perfectament vàlid per declarar.

Declarar el constructor public en una classe abstracta. Compila, però menteix: suggereix un accés que no existeix. Fes servir protected.

Combinar abstract amb final, static o private. Les tres combinacions són contradictòries i el compilador les rebutja. abstract significa "cal sobreescriure'l"; final significa "no es pot", static significa "no és polimòrfic" i private significa "no es veu".

Descuidar que la subclasse parcial ha de continuar sent abstracta. Si implementes dos de tres mètodes abstractes, la classe resultant continua incompleta.

Triar classe abstracta per costum. És la trampa de disseny més cara del mòdul. En obligar a extends, gastes l'única herència dels teus usuaris. Comença sempre per la interfície i afegeix la base abstracta només si hi ha estat o codi real per compartir.

Cridar un mètode abstracte (o sobreescrivible) des del constructor de la base. És un error subtil i greu: quan s'executa el constructor de Material, els camps de Llibre encara no estan inicialitzats. Si Material cridés al seu constructor un descriure() que fa servir autor, obtindries null. Regla: des d'un constructor, invoca només mètodes private, static o final.

Consell: final al mètode plantilla. És el que converteix una jerarquia en un Template Method de debò. Si l'esquelet no és final, qualsevol subclasse es pot saltar la política.

Consell: documenta el contracte de cada mètode abstracte. El Javadoc d'un mètode abstracte és l'únic lloc on pots dir a qui l'implementi què s'espera d'ell: què ha de retornar, quins rangs són vàlids, si pot retornar null. És disseny per contracte (03-08) aplicat al punt exacte on més falta fa.

Exercicis

Exercici 1: AudioLlibre

Afegeix a BiblioTech un suport AudioLlibre extends Material amb un camp propi duracioHores (double) i un mètode getNarrador(). Termini: 10 dies. Tarifa: 0,15 €/dia. Canal d'avís: "correu" (l'heretat). Comprova que el compilador t'obliga a implementar els tres mètodes abstractes i integra el nou suport a l'array cataleg sense tocar el bucle que el recorre.

Exercici 2: Template Method per al rebut

Crea una classe abstracta InformeMaterial al paquet presentacio amb:

  • Un mètode public final String generar(Material m, int diesTranscorreguts) que retorni capcalera() + cos(m, dies) + peu().
  • capcalera() i peu() com a mètodes concrets amb una implementació per defecte.
  • cos(Material, int) com a mètode abstracte.

Crea dues subclasses: InformeBreu (una línia amb títol i multa) i InformeDetallat (títol, tipus, termini, retard, multa i gravetat, un per línia). Comprova que l'esquelet s'escriu una sola vegada.

Exercici 3: triar interfície o classe abstracta

Per a cada cas, decideix si faries servir interfície, classe abstracta o totes dues, i justifica-ho en una frase:

  1. Tot objecte de BiblioTech que es pugui exportar a un fitxer de text.
  2. La base comuna de tres tipus d'empleat (Intern, Extern, Becari) que comparteixen nom, identificador i el límit de préstecs, però amb un màxim diferent.
  3. La capacitat de comparar dos materials per ordenar-los.
  4. Un catàleg de préstecs amb diferents implementacions futures: en memòria, en fitxer i en base de dades.

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;

    private final String narrador;
    private final double duracioHores;

    public AudioLlibre(String titol, String referencia,
                       String narrador, double duracioHores) {
        super(titol, referencia, true);           // validacio centralitzada a Material
        this.narrador     = (narrador == null || narrador.isBlank())
                            ? "Desconegut" : narrador.trim();
        this.duracioHores = Math.max(0.0, duracioHores);
    }

    public String getNarrador()     { return narrador; }
    public double getDuracioHores() { return duracioHores; }

    // Els tres metodes que el compilador EXIGEIX:
    @Override public String getTipus()        { return "Audiollibre"; }
    @Override public int    getDiesPrestec()  { return DIES_PRESTEC_AUDIO; }
    @Override public double getTarifaDiaria() { return TARIFA_DIARIA_AUDIO; }

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

Si ometes, per exemple, getTarifaDiaria():

error: AudioLlibre is not abstract and does not override
       abstract method getTarifaDiaria() in Material

I la integració:

Material[] cataleg = {
    new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
    new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
    new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
    new AudioLlibre("Patrons de Disseny", "AUD-0002", "Nuria Vidal", 12.5)   // unica linia nova
};
TIPUS       TERMINI  TARIFA   MULTA@20d  GRAVETAT
Llibre      15       0,25     1,25       LLEU
Revista     7        0,10     1,30       GREU
DVD         3        0,50     8,50       GREU
Audiollibre 10       0,15     1,50       GREU

El bucle no s'ha tocat. Un suport nou costa una classe i una línia de dades: zero modificacions a la lògica existent. I la novetat respecte al mòdul 3 és que el compilador ha garantit que vas declarar termini, tarifa i tipus. Amb la versió antiga de Material, descuidar la tarifa hauria cobrat silenciosament 0,25 €/dia.

Solució 2

package com.nexussoftware.bibliotech.presentacio;

import com.nexussoftware.bibliotech.domini.Material;

/** Base de tots els informes de material. Defineix l'esquelet; no el contingut. */
public abstract class InformeMaterial {

    private static final String LINIA = "----------------------------------------";

    /**
     * TEMPLATE METHOD: l'estructura de l'informe es fixa (final)
     * i l'unic pas variable es el cos.
     */
    public final String generar(Material m, int diesTranscorreguts) {
        return capcalera() + cos(m, diesTranscorreguts) + peu();
    }

    /** Pas concret: valid per defecte, sobreescrivible si cal. */
    protected String capcalera() {
        return LINIA + System.lineSeparator()
             + "   BIBLIOTECH - NEXUS SOFTWARE" + System.lineSeparator()
             + LINIA + System.lineSeparator();
    }

    /** Pas concret. */
    protected String peu() {
        return LINIA + System.lineSeparator();
    }

    /** PAS VARIABLE: cada informe decideix que mostra. */
    protected abstract String cos(Material m, int diesTranscorreguts);
}
package com.nexussoftware.bibliotech.presentacio;

import com.nexussoftware.bibliotech.domini.Material;

/** Una sola linia: per a llistats llargs. */
public class InformeBreu extends InformeMaterial {

    @Override
    protected String cos(Material m, int diesTranscorreguts) {
        return String.format("  %-28s %6.2f EUR%n",
                             m.getTitol(), m.calcularMulta(diesTranscorreguts));
    }
}
package com.nexussoftware.bibliotech.presentacio;

import com.nexussoftware.bibliotech.domini.Material;

/** Informe complet: per a incidencies i reclamacions. */
public class InformeDetallat extends InformeMaterial {

    @Override
    protected String cos(Material m, int diesTranscorreguts) {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("  %-14s %s%n",       "Titol:",    m.getTitol()));
        sb.append(String.format("  %-14s %s%n",       "Tipus:",    m.getTipus()));
        sb.append(String.format("  %-14s %d dies%n",  "Termini:",  m.getDiesPrestec()));
        sb.append(String.format("  %-14s %d dies%n",  "Retard:",   m.calcularDiesRetard(diesTranscorreguts)));
        sb.append(String.format("  %-14s %.2f EUR%n", "Multa:",    m.calcularMulta(diesTranscorreguts)));
        sb.append(String.format("  %-14s %s%n",       "Gravetat:", m.classificarGravetat(diesTranscorreguts)));
        return sb.toString();
    }
}
Material dvd = new Dvd("Refactoritzacio en directe", "DVD-0007", 95);

InformeMaterial breu     = new InformeBreu();
InformeMaterial detallat = new InformeDetallat();

System.out.print(breu.generar(dvd, 20));
System.out.print(detallat.generar(dvd, 20));
----------------------------------------
   BIBLIOTECH - NEXUS SOFTWARE
----------------------------------------
  Refactoritzacio en directe     8,50 EUR
----------------------------------------
----------------------------------------
   BIBLIOTECH - NEXUS SOFTWARE
----------------------------------------
  Titol:         Refactoritzacio en directe
  Tipus:         DVD
  Termini:       3 dies
  Retard:        17 dies
  Multa:         8,50 EUR
  Gravetat:      GREU
----------------------------------------

Quatre detalls de disseny: generar és final (l'estructura no es negocia); cos és protected perquè és un punt d'extensió per a les subclasses i no part de l'API pública; capcalera i peu són concrets i protected, així que una futura subclasse els pot sobreescriure sense veure-s'hi obligada; i la variable es declara InformeMaterial, no InformeBreu, així que canviar d'informe costa una paraula.

Solució 3

Cas Decisió Justificació
1. Exportable a text Interfície (Exportable amb String aLinia()) És una capacitat que han de signar classes sense parentiu (Material, Empleat, Prestec, SalaReunions), i no requereix estat compartit
2. Base de tres tipus d'empleat Classe abstracta (Empleat amb getMaxPrestecs() abstracte) Comparteixen camps (nom, identificador, prestecsAcumulats), constructor amb validació i lògica real; formen una família genuïna i només varia un valor
3. Comparar materials Interfície, i a més una del JDK: Comparable<Material> És pur comportament sense estat, ja existeix a la biblioteca estàndard i no ha de consumir l'herència. S'aplica a 04-04 i 05-09
4. Catàleg amb diverses implementacions Totes dues: interfície CatalegPrestecs més base AbstractCatalegPrestecs La interfície és el tipus públic del qual depenen les regles de negoci (inversió de dependències, 04-01); la base abstracta estalvia a les tres implementacions el codi repetit de validació i format, sense obligar ningú a fer-la servir

El cas 4 és l'arquetip professional i el que veuràs una vegada i una altra a partir del mòdul 5: la interfície defineix el tipus, la classe abstracta ofereix ajuda opcional, i les classes concretes trien.

Conclusió

Has completat la parella que sosté el disseny en Java. Saps que abstract en una classe declara que està conceptualment incompleta i que impedeix new, sense impedir que la facis servir com a tipus de variables, paràmetres i arrays. Saps que un mètode abstracte és una obligació que es transmet cap avall fins que una subclasse concreta la compleix, i —el més important— per què aquella obligació val més que qualsevol implementació de farciment: converteix un oblit silenciós, que apareix setmanes després com una multa mal calculada, en un error de compilació instantani.

Entens el que una classe abstracta ofereix i una interfície no podrà oferir mai: camps d'instància, constructor i membres protected. Saps per a què serveix el constructor d'una classe que no s'instancia —s'executa sempre via super(...), centralitza la validació comuna i garanteix els invariants de la part compartida— i per què es declara protected. Tens la taula comparativa completa entre interfície i classe abstracta, inclosa la fila que més pesa a la pràctica: la classe abstracta consumeix l'única herència de qui la fa servir, i per això el camí professional comença sempre per la interfície.

I has descobert el Template Method escrivint-lo, no llegint-lo: un mètode final que fixa l'esquelet de l'algorisme i delega els passos variables en mètodes abstractes. És el que permet que la fórmula de la multa —retard per tarifa, amb sostre de 20 €— existeixi una sola vegada a tot BiblioTech i produeixi resultats diferents i correctes per a un llibre, una revista, un DVD i qualsevol suport que afegeixis demà. Aquest patró es formalitza a 12-02. Finalment, saps reconèixer el patró AbstractXxx del JDK, on interfície i classe abstracta no competeixen, sinó que es reparteixen la feina: la primera és el tipus públic, la segona una ajuda opcional.

BiblioTech queda amb una jerarquia honesta: Material és abstracta i signa Prestable i Notificable; Llibre, Revista, Dvd declaren obligatòriament el seu tipus, el seu termini i la seva tarifa; i SalaReunions demostra que es pot signar el contracte sense entrar a la família.

Però fixa't en una peça que arrossegues des de 03-07 i que continua sent poc satisfactòria: les incidències d'un Prestec són un String[] de textos solts. Una incidència real té un dia, un motiu i una gravetat, i mereix ser un tipus propi. Ara bé, una classe de primer nivell, en el seu propi fitxer, per a una cosa que només té sentit dins d'un préstec? A la lliçó 04-03, Classes Internes, veuràs els quatre tipus de classe imbricada que ofereix Java, aprendràs quan cadascun és l'elecció correcta —inclosa la que causa fuites de memòria reals en producció— i convertiràs aquelles cadenes soltes en una Prestec.Incidencia amb nom, estructura i encapsulament propis.

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