Arribes al tancament del mòdul amb dos deutes pendents que arrossegues des del mòdul 3. El primer: classificarGravetat retorna les cadenes "LLEU" i "GREU", que tot el projecte compara amb equals. Res no impedeix escriure "greu" en minúscules, "GRAVISSIM" o "LEU", i el compilador no dirà ni una paraula; la fallada apareixerà en producció, en forma d'una multa mal classificada. El segon: has escrit a mà equals, hashCode i toString a Material, a Prestec.Incidencia i a cada classe de dades del projecte —les mateixes vint línies, tres vegades, amb el risc d'equivocar-te en qualsevol d'elles.

Java té una resposta exacta per a cada deute. Les enumeracions (enum) converteixen un conjunt de valors fixos en un tipus propi: Gravetat.GREU no és una cadena, és un valor d'un conjunt tancat que el compilador coneix, amb el qual un error d'escriptura és impossible i sobre el qual un switch pot comprovar que has cobert tots els casos. Els registres (record, Java 16) generen automàticament el constructor, els accessors, equals, hashCode i toString d'una classe immutable de dades, reduint cinquanta línies a una. En acabar aquesta lliçó, BiblioTech estarà complet com a projecte de POO avançada, i veuràs amb claredat què li continua faltant per ser un sistema de debò.

Contingut

  1. El problema: constants com a String o int
  2. Què és un enum
  3. Mètodes implícits: values, valueOf, name, ordinal
  4. Comparació d'enums: == enfront d'equals
  5. enum amb camps, constructor i mètodes
  6. enum amb cos per constant
  7. enum en switch i exhaustivitat
  8. El singleton amb enum
  9. Registres: què és un record
  10. Què genera un record automàticament
  11. Constructor compacte i validació
  12. Constructors addicionals i mètodes propis
  13. Què NO pot fer un record i quan no fer-lo servir
  14. Records a BiblioTech: DTO i objectes de valor
  15. Taula comparativa: classe, record i enum
  16. Tancament del mòdul: estat de BiblioTech
  17. Errors Habituals i Consells
  18. Exercicis

  1. El problema: constants com a String o int

Aquest és el mètode que arrossegues des de 03-07:

public final String classificarGravetat(int diesTranscorreguts) {
    int retard = calcularDiesRetard(diesTranscorreguts);
    if (retard == 0)            { return "SENSE RETARD"; }
    if (retard <= LLINDAR_LLEU) { return "LLEU"; }
    return "GREU";
}

I així es fa servir per tot el projecte:

if (prestec.classificarGravetat().equals("GREU")) {
    escalarIncidencia(prestec);
}

Enumerem els problemes, perquè en són cinc i tots seriosos:

Problema Conseqüència
Sense seguretat de tipus equals("greu"), equals("GRUE") i equals("URGENT") compilen sense protestar i retornen false sempre
Sense exhaustivitat Si afegeixes un tercer nivell de gravetat, res no t'avisa dels deu if que cal actualitzar
Sense autocompletat L'IDE no pot suggerir els valors vàlids: són cadenes qualssevol
Sense comportament La cadena "GREU" no sap quin és el seu canal d'avís ni el seu llindar
Cost de comparació Comparar cadenes recorre caràcters; comparar referències és una instrucció

L'alternativa tradicional era pitjor: constants int.

public static final int GRAVETAT_SENSE_RETARD = 0;
public static final int GRAVETAT_LLEU         = 1;
public static final int GRAVETAT_GREU         = 2;

// ...i res no impedeix aixo:
processarGravetat(47);          // compila perfectament
processarGravetat(diesRetard);  // s'hi cola un valor d'un altre domini

Aquest antipatró té nom a la literatura: int enum pattern. Java 5 el va resoldre d'una vegada.

  1. Què és un enum

Un enum és un tipus els valors possibles del qual són un conjunt fix i conegut en temps de compilació. En la seva forma més simple:

package com.nexussoftware.bibliotech.domini;

/** Grau de gravetat del retard d'un prestec. */
public enum Gravetat {
    SENSE_RETARD,
    LLEU,
    GREU
}

Amb aquella declaració, Gravetat és un tipus tan real com String o Material:

Gravetat g = Gravetat.GREU;

// Gravetat g2 = "GREU";         // NO COMPILA: String no es Gravetat
// Gravetat g3 = Gravetat.GREU2; // NO COMPILA: no existeix aquesta constant

Sota el capó, un enum és una classe que hereta de java.lang.Enum i les constants de la qual són instàncies public static final d'aquella classe, creades per la JVM en carregar el tipus. Tres conseqüències:

  • No pots instanciar un enum amb new. El seu constructor és implícitament private.
  • Existeix exactament una instància de cada constant a tota la JVM. Són singletons per construcció.
  • És una classe de ple dret: pot tenir camps, mètodes, constructors i implementar interfícies (apartat 5).

I classificarGravetat millora immediatament:

public final Gravetat classificarGravetat(int diesTranscorreguts) {
    int retard = calcularDiesRetard(diesTranscorreguts);
    if (retard == 0)            { return Gravetat.SENSE_RETARD; }
    if (retard <= LLINDAR_LLEU) { return Gravetat.LLEU; }
    return Gravetat.GREU;
}
if (prestec.classificarGravetat() == Gravetat.GREU) {   // == en lloc d'equals
    escalarIncidencia(prestec);
}
// if (prestec.classificarGravetat() == Gravetat.URGENT)  // NO COMPILA

L'error d'escriptura ha passat de ser una fallada silenciosa en producció a un error de compilació.

  1. Mètodes implícits: values, valueOf, name, ordinal

Tot enum rep de franc un conjunt de mètodes, alguns heretats de java.lang.Enum i d'altres generats pel compilador.

Mètode Tipus Què retorna
values() static Un array amb totes les constants, en ordre de declaració
valueOf(String) static La constant amb aquell nom exacte (o IllegalArgumentException)
name() instància El nom exacte de la constant, com a text
ordinal() instància La seva posició (base 0) a la declaració
compareTo(E) instància Comparació per ordinal
toString() instància Per defecte igual que name(); sí que es pot sobreescriure
// Recorrer totes les constants
for (Gravetat g : Gravetat.values()) {
    System.out.printf("%d -> %s%n", g.ordinal(), g.name());
}
0 -> SENSE_RETARD
1 -> LLEU
2 -> GREU
// De text a enum: util en llegir configuracio o entrada de l'usuari
Gravetat llegida = Gravetat.valueOf("GREU");
System.out.println(llegida == Gravetat.GREU);    // true

// Gravetat dolenta = Gravetat.valueOf("greu");  // IllegalArgumentException en execucio

valueOf distingeix majúscules i llança excepció si no troba la constant. Com que les excepcions són el mòdul 6, mentrestant convé un mètode de conversió tolerant escrit a mà:

/** Converteix text a Gravetat sense llancar excepcions. Retorna SENSE_RETARD si no encaixa. */
public static Gravetat desDeText(String text) {
    if (text == null) { return SENSE_RETARD; }
    String net = text.trim().toUpperCase().replace(' ', '_');
    for (Gravetat g : values()) {
        if (g.name().equals(net)) { return g; }
    }
    return SENSE_RETARD;
}

I ara l'advertència important: no depenguis d'ordinal().

// MALAMENT: la logica depen de la POSICIO a la declaracio
if (gravetat.ordinal() >= 2) {
    escalarIncidencia();
}

Aquell codi funciona avui. Però si demà algú afegeix MOLT_LLEU entre SENSE_RETARD i LLEU, tots els ordinals es desplacen i el >= 2 passa a significar una altra cosa sense que res falli ni avisi. El mateix val per desar l'ordinal() en un fitxer o una base de dades: reordenar les constants corromp les dades desades.

L'alternativa correcta és un camp explícit:

public enum Gravetat {
    SENSE_RETARD(0),
    LLEU(1),
    GREU(2);

    private final int nivell;                   // valor explicit i estable
    Gravetat(int nivell) { this.nivell = nivell; }
    public int getNivell() { return nivell; }
}
if (gravetat.getNivell() >= 2) { escalarIncidencia(); }   // estable davant de reordenacions

L'ordinal() existeix perquè les col·leccions especialitzades del JDK (EnumMap, EnumSet) el fan servir internament. El teu codi no l'hauria de tocar mai.

  1. Comparació d'enums: == enfront d'equals

Amb els enums, == és la forma correcta i preferida. És l'única situació del curs en què això és cert per a objectes.

Gravetat a = Gravetat.GREU;
Gravetat b = prestec.classificarGravetat();

if (a == b)        { }    // CORRECTE i preferit
if (a.equals(b))   { }    // funciona, pero innecessari

Quatre raons, que recuperen el que vas aprendre a 01-05 i 03-09:

  1. Només hi ha una instància de cada constant. La igualtat de referència i la igualtat lògica coincideixen per construcció. Enum.equals és final i està implementat exactament com this == other.
  2. == és segur davant de null. a == null dona false; a.equals(b) amb a nul llança NullPointerException.
  3. == dona seguretat de tipus. Comparar dos enums de tipus diferents amb == no compila; amb equals compila i retorna false sempre, amagant l'error.
  4. És més ràpid: una comparació de referències enfront d'una crida a mètode.
Gravetat g = Gravetat.LLEU;
Periodicitat p = Periodicitat.MENSUAL;

// if (g == p)            // NO COMPILA: tipus incomparables. Perfecte.
if (g.equals(p)) { }      // compila i sempre es false: bug silencios

  1. enum amb camps, constructor i mètodes

Aquí és on els enums de Java superen amb escreix els d'altres llenguatges. Una constant d'enum pot portar dades associades.

Aplicat al projecte: Llibre, Revista i Dvd existeixen com a classes perquè cadascuna té el seu termini i la seva tarifa. Però si aquella fos tota la diferència —només dades, sense comportament propi ni camps exclusius— la jerarquia sencera sobraria.

package com.nexussoftware.bibliotech.domini;

/**
 * Tipus de material del cataleg, amb els seus parametres de prestec.
 *
 * <p>Cada constant porta les seves dades associades: substitueix una jerarquia
 * sencera quan l'unic que varia son valors, no comportament.</p>
 */
public enum TipusMaterial {

    // Cada constant invoca el constructor amb ELS SEUS valors
    LLIBRE ("Llibre",   15, 0.25, "correu"),
    REVISTA("Revista",   7, 0.10, "xat"),
    DVD    ("DVD",       3, 0.50, "telefon"),
    AUDIO  ("Audiollibre", 10, 0.15, "correu");   // el punt i coma es OBLIGATORI

    private final String etiqueta;
    private final int    diesPrestec;
    private final double tarifaDiaria;
    private final String canalAvis;

    /** El constructor d'un enum es implicitament private. */
    TipusMaterial(String etiqueta, int diesPrestec,
                  double tarifaDiaria, String canalAvis) {
        this.etiqueta     = etiqueta;
        this.diesPrestec  = diesPrestec;
        this.tarifaDiaria = tarifaDiaria;
        this.canalAvis    = canalAvis;
    }

    public String getEtiqueta()     { return etiqueta; }
    public int    getDiesPrestec()  { return diesPrestec; }
    public double getTarifaDiaria() { return tarifaDiaria; }
    public String getCanalAvis()    { return canalAvis; }

    /** Metode de negoci propi del tipus. */
    public double multaPer(int diesRetard) {
        return Math.min(Math.max(0, diesRetard) * tarifaDiaria, 20.0);
    }

    public boolean esTerminiCurt() { return diesPrestec <= 7; }

    @Override
    public String toString() { return etiqueta; }
}

Punts de sintaxi que cal fixar:

  • Les constants van primer, abans que cap camp o mètode.
  • El punt i coma després de l'última constant és obligatori si hi ha alguna cosa més al cos. És l'error de compilació més comú amb enums.
  • El constructor és implícitament private i no pot ser cap altra cosa. S'invoca una sola vegada per constant, en carregar la classe.
  • Els camps han de ser final. Tècnicament es permeten mutables, però un enum mutable és un error de disseny: la constant és única i compartida per tot el programa.

I el que ara s'hi pot fer:

System.out.printf("%-12s %-8s %-8s %-10s %s%n",
                  "TIPUS", "TERMINI", "TARIFA", "CANAL", "MULTA@17d");

for (TipusMaterial t : TipusMaterial.values()) {
    System.out.printf("%-12s %-8d %-8.2f %-10s %.2f EUR%n",
                      t.getEtiqueta(), t.getDiesPrestec(), t.getTarifaDiaria(),
                      t.getCanalAvis(), t.multaPer(17));
}
TIPUS        TERMINI  TARIFA   CANAL      MULTA@17d
Llibre       15       0,25     correu     4,25 EUR
Revista      7        0,10     xat        1,70 EUR
DVD          3        0,50     telefon    8,50 EUR
Audiollibre  10       0,15     correu     2,55 EUR

Substitueix això la jerarquia Material? No del tot, i aquesta és la lliçó de disseny:

Situació Tria
Els tipus només difereixen en valors (termini, tarifa, canal) enum: quatre línies enfront de quatre classes
Els tipus tenen camps propis diferents Jerarquia de classes
Els tipus tenen comportament estructuralment diferent Jerarquia de classes
Totes dues coses Jerarquia amb un enum com a camp

A BiblioTech es compleix la tercera: Llibreautor i anyPublicacio, Revistanumero i periodicitat, DvdduracioMinuts. La jerarquia es queda. Però TipusMaterial continua sent útil com a camp que centralitza els paràmetres comuns, i l'aplicaràs a l'apartat 16.

  1. enum amb cos per constant

Una volta de rosca més: cada constant pot tenir la seva pròpia implementació d'un mètode. La sintaxi és un cos { ... } després dels arguments del constructor.

package com.nexussoftware.bibliotech.domini;

/** Gravetat del retard, amb l'accio que correspon a cada nivell. */
public enum Gravetat {

    SENSE_RETARD(0) {
        @Override
        public String accioRecomanada() {
            return "Cap. El prestec esta en termini.";
        }
        @Override
        public boolean requereixAvis() { return false; }
    },

    LLEU(1) {
        @Override
        public String accioRecomanada() {
            return "Enviar recordatori pel canal habitual.";
        }
        @Override
        public boolean requereixAvis() { return true; }
    },

    GREU(2) {
        @Override
        public String accioRecomanada() {
            return "Escalar al responsable i bloquejar nous prestecs.";
        }
        @Override
        public boolean requereixAvis() { return true; }
    };

    private final int nivell;

    Gravetat(int nivell) { this.nivell = nivell; }

    public int getNivell() { return nivell; }

    /** Cada constant l'HA d'implementar: es abstracte. */
    public abstract String accioRecomanada();

    /** Cada constant l'implementa; podria tenir implementacio per defecte. */
    public abstract boolean requereixAvis();

    /** Metode comu a totes les constants. */
    public boolean esMesGreuQue(Gravetat altra) {
        return this.nivell > altra.nivell;
    }
}
for (Gravetat g : Gravetat.values()) {
    System.out.printf("%-12s nivell %d  avis=%-5b  %s%n",
                      g, g.getNivell(), g.requereixAvis(), g.accioRecomanada());
}
System.out.println("GREU mes greu que LLEU: " + Gravetat.GREU.esMesGreuQue(Gravetat.LLEU));
SENSE_RETARD nivell 0  avis=false  Cap. El prestec esta en termini.
LLEU         nivell 1  avis=true   Enviar recordatori pel canal habitual.
GREU         nivell 2  avis=true   Escalar al responsable i bloquejar nous prestecs.
GREU mes greu que LLEU: true

Què està passant aquí realment? Cada constant amb cos és una subclasse anònima de l'enum (04-04). Ho pots comprovar:

System.out.println(Gravetat.LLEU.getClass().getName());   // ...Gravetat$2
System.out.println(Gravetat.class.getName());             // ...Gravetat

La conseqüència pràctica és enorme: el compilador obliga a implementar el mètode abstracte a cada constant. Si demà afegeixes MOLT_GREU sense accioRecomanada(), el codi no compila. Compara-ho amb un switch sobre cadenes, on el cas nou simplement cau al default i ningú no se n'assabenta.

Cos per constant o switch? Fes servir cos per constant quan la lògica pertany al valor i varia a cadascun. Fes servir camps i mètodes comuns quan la lògica és la mateixa amb dades diferents. I si el cos per constant comença a tenir vint línies, l'enum probablement estigui assumint responsabilitats alienes.

  1. enum en switch i exhaustivitat

Els enums i el switch estan fets l'un per a l'altre. I hi ha un detall de sintaxi que sorprèn:

public String missatgePer(Gravetat gravetat) {
    return switch (gravetat) {
        case SENSE_RETARD -> "Tot en ordre.";              // sense 'Gravetat.'
        case LLEU         -> "Recordatori enviat.";
        case GREU         -> "Incidencia escalada.";
    };
}

Dins del case no es qualifica la constant: s'escriu LLEU, no Gravetat.LLEU. El compilador ja sap de quin tipus és l'expressió del switch, i qualificar-la és de fet un error de compilació.

I ara el que és veritablement valuós, reprenent el switch d'expressió de 02-03: l'exhaustivitat.

// switch d'EXPRESSIO sobre un enum: no cal 'default'
return switch (gravetat) {
    case SENSE_RETARD -> "Tot en ordre.";
    case LLEU         -> "Recordatori enviat.";
    case GREU         -> "Incidencia escalada.";
};

Com que un switch d'expressió ha de produir sempre un valor, el compilador verifica que has cobert totes les constants. Si demà afegeixes MOLT_GREU a l'enum:

error: the switch expression does not cover all possible input values

El compilador et porta de la mà a cada lloc del projecte que cal actualitzar. Això és impossible amb cadenes i és la raó número u per fer servir enums al domini.

Una advertència sobre el default:

// Amb default: compila avui i compilara dema... fent alguna cosa malament
return switch (gravetat) {
    case SENSE_RETARD -> "Tot en ordre.";
    case LLEU         -> "Recordatori enviat.";
    default           -> "Incidencia escalada.";   // MOLT_GREU cauria aqui en silenci
};

Regla: en un switch d'expressió sobre un enum, omet el default sempre que puguis. És el que et dona la comprovació d'exhaustivitat, que és justament el que has vingut a buscar.

En un switch clàssic de sentència (amb case ... :) l'exhaustivitat no es comprova, perquè no hi ha obligació de produir un valor. Un altre motiu per preferir la forma de fletxa.

  1. El singleton amb enum

Una aplicació breu però coneguda: com que la JVM garanteix una única instància de cada constant, un enum d'un sol valor és la forma més simple i segura d'implementar el patró Singleton (formalitzat a 12-02), immune a la reflexió i a la serialització.

public enum RegistreBiblioTech {
    INSTANCIA;

    private int operacions;

    public void registrar(String operacio) {
        operacions++;
        System.out.println("[" + operacions + "] " + operacio);
    }
}

// Us:
RegistreBiblioTech.INSTANCIA.registrar("Prestec PR-0001 creat");

  1. Registres: què és un record

Canviem d'eina. Mira aquesta classe, que només existeix per transportar tres dades:

public final class Fitxa {

    private final String titol;
    private final String autor;
    private final int    any;

    public Fitxa(String titol, String autor, int any) {
        this.titol = titol;
        this.autor = autor;
        this.any   = any;
    }

    public String getTitol() { return titol; }
    public String getAutor() { return autor; }
    public int    getAny()   { return any;   }

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        Fitxa altra = (Fitxa) o;
        return any == altra.any
            && Objects.equals(titol, altra.titol)
            && Objects.equals(autor, altra.autor);
    }

    @Override
    public int hashCode() { return Objects.hash(titol, autor, any); }

    @Override
    public String toString() {
        return "Fitxa[titol=" + titol + ", autor=" + autor + ", any=" + any + "]";
    }
}

Gairebé quaranta línies, i l'única informació real són tres noms i tres tipus. Tota la resta és mecànica, i per ser-ho és propensa a errors: descuidar un camp a equals, no actualitzar hashCode en afegir-ne un, deixar un toString desfasat.

Un record (Java 16) expressa exactament el mateix:

public record Fitxa(String titol, String autor, int any) { }

Una línia. I és completament equivalent, amb els tres mètodes correctament implementats.

La declaració es llegeix així:

record NomDelTipus(Tipus component1, Tipus component2, ...) { cos opcional }

Als paràmetres de la capçalera se'ls anomena components. Un record és, per disseny, un portador transparent de dades immutables: transparent perquè els seus components són exactament el seu estat i es poden llegir tots.

  1. Què genera un record automàticament

D'aquella única línia, el compilador genera:

Element generat Detall
Un camp private final per component No accessible directament ni des del mateix record llevat que sigui pel seu nom
Constructor canònic Rep tots els components en ordre i els assigna
Un accessor per component Es diu com el component: titol(), no getTitol()
equals(Object) Compara tots els components; compleix el contracte de 03-09
hashCode() Derivat de tots els components; consistent amb equals
toString() Fitxa[titol=Java Eficac, autor=Joshua Bloch, any=2018]
Fitxa f1 = new Fitxa("Java Eficac", "Joshua Bloch", 2018);
Fitxa f2 = new Fitxa("Java Eficac", "Joshua Bloch", 2018);

System.out.println(f1.titol());             // Java Eficac  (sense 'get')
System.out.println(f1);                     // Fitxa[titol=Java Eficac, ...]
System.out.println(f1.equals(f2));          // true
System.out.println(f1.hashCode() == f2.hashCode());   // true
System.out.println(f1 == f2);               // false: son objectes diferents

Fixa't en el detall dels accessors: es diuen titol(), no getTitol(). No és un caprici: és un senyal deliberat del disseny del llenguatge que un record no és un JavaBean, sinó un valor. Si necessites el conveni getXxx perquè un framework l'exigeix (Hibernate, per exemple, mòdul 11), l'hi pots afegir a mà, però sol ser símptoma que allà volies una classe normal.

I tres propietats estructurals importants:

  • Un record és implícitament final. No es pot estendre.
  • Els seus components són immutables. No hi ha setters ni manera de reassignar-los.
  • Hereta de java.lang.Record, no d'Object directament. Per això un record no pot estendre cap altra classe (apartat 13).

  1. Constructor compacte i validació

Un record sense validació acceptaria un títol nul o un any impossible. La solució és el constructor compacte, una forma abreujada exclusiva dels records:

public record Fitxa(String titol, String autor, int any) {

    /**
     * Constructor COMPACTE: sense llista de parametres ni assignacions.
     * S'executa abans d'assignar els camps, sobre els parametres.
     */
    public Fitxa {
        if (titol == null || titol.isBlank()) {
            titol = "Sense titol";                 // es reassigna el PARAMETRE
        }
        if (autor == null || autor.isBlank()) {
            autor = "Desconegut";
        }
        if (any < 1450 || any > 2100) {
            any = 0;
        }
        titol = titol.trim();
        autor = autor.trim();
        // NO s'escriu this.titol = titol; el compilador ho fa al final
    }
}
System.out.println(new Fitxa("  Java Eficac  ", null, 3000));
Fitxa[titol=Java Eficac, autor=Desconegut, any=0]

Tres regles del constructor compacte:

  1. No porta llista de paràmetres: s'escriu public Fitxa {, no public Fitxa(String titol, ...) {.
  2. A dins treballes amb els paràmetres, no amb els camps. titol = "Sense titol" canvia el paràmetre; el compilador assigna els camps al final, automàticament.
  3. No escriguis this.titol = titol: és innecessari i confús.

Això és exactament el sanejament de dades que feies a mà als constructors del mòdul 3, ara al seu lloc natural. Tan bon punt tinguis excepcions (mòdul 6), l'habitual serà llançar IllegalArgumentException en lloc de substituir valors; el mecanisme del constructor compacte és el mateix.

  1. Constructors addicionals i mètodes propis

El cos d'un record admet molt més que el constructor compacte.

Constructors addicionals, que han de delegar en el canònic amb this(...) (03-04):

public record Fitxa(String titol, String autor, int any) {

    public Fitxa {
        if (titol == null || titol.isBlank()) { titol = "Sense titol"; }
    }

    /** Fitxa sense any conegut. */
    public Fitxa(String titol, String autor) {
        this(titol, autor, 0);           // OBLIGATORI delegar en el canonic
    }

    /** Fitxa construida des d'un material del cataleg. */
    public static Fitxa desDe(Llibre llibre) {
        return new Fitxa(llibre.getTitol(), llibre.getAutor(), llibre.getAnyPublicacio());
    }
}

Mètodes d'instància propis, que operen sobre els components:

public record Fitxa(String titol, String autor, int any) {

    public boolean esClassic() {
        return any > 0 && any < 2000;
    }

    public String etiquetaCurta() {
        String curt = titol.length() > 20 ? titol.substring(0, 17) + "..." : titol;
        return String.format("%-20s (%s)", curt, any > 0 ? String.valueOf(any) : "s.d.");
    }

    /** "Modificador" a l'estil immutable: retorna una copia nova. */
    public Fitxa ambAny(int nouAny) {
        return new Fitxa(titol, autor, nouAny);
    }
}

Aquell ambAny és el patró estàndard amb tipus immutables (03-07): no es modifica, es crea una còpia amb el canvi. És el que fa String amb toUpperCase() i el que faràs amb LocalDate a 10-05.

Constants, mètodes static i tipus imbricats, tot permès:

public record Fitxa(String titol, String autor, int any) {
    public static final Fitxa BUIDA = new Fitxa("Sense titol", "Desconegut", 0);
    public static int comparar(Fitxa a, Fitxa b) { return a.titol.compareTo(b.titol); }
}

Implementar interfícies, sí; estendre classes, no:

public record Fitxa(String titol, String autor, int any)
        implements Comparable<Fitxa> {

    @Override
    public int compareTo(Fitxa altra) {
        return this.titol.compareToIgnoreCase(altra.titol);
    }
}

I també es pot sobreescriure un accessor o un mètode generat, encara que convé fer-ho amb moderació:

public record Fitxa(String titol, String autor, int any) {
    @Override
    public String toString() {
        return titol + " - " + autor + (any > 0 ? " (" + any + ")" : "");
    }
}

  1. Què NO pot fer un record i quan no fer-lo servir

No pot Per què
Estendre una classe Ja estén java.lang.Record
Ser estès És implícitament final
Declarar camps d'instància propis El seu estat són exactament els seus components; és la garantia de transparència
Tenir components mutables Són final per construcció
Ser abstract No té sentit en un portador de dades

Aquest tercer punt sorprèn molta gent:

public record Fitxa(String titol, String autor, int any) {
    // private int comptadorAccessos;   // NO COMPILA: camp d'instancia
    private static int fitxesCreades;   // SI QUE COMPILA: els static si que es permeten
}

La raó és el principi de disseny: l'estat d'un record és la seva llista de components, sense excepcions. Només així equals, hashCode i toString generats poden ser correctes per construcció.

Quan NO fer servir un record:

  • Quan l'objecte té identitat, no valor. Un Empleat amb EMP-001 continua sent el mateix empleat encara que canviï de nom; dues Fitxa amb les mateixes dades són la mateixa fitxa. La pregunta clau: dos objectes amb les mateixes dades són el mateix objecte? Si sí, record.
  • Quan l'estat ha de canviar. Un Prestec canvia de disponibilitat, acumula incidències i registra devolucions. No és un record.
  • Quan vols amagar la representació interna. Un record exposa tots els seus components. Si necessites encapsular la representació (03-07), fes servir una classe.
  • Quan necessites heretar. Un record no pot estendre ni ser estès.
  • Quan l'API pública ha de seguir el conveni getXxx per exigència d'un framework antic.
flowchart TD
    A["Necessito un tipus de dades"] --> B{"El seu estat canvia amb el temps?"}
    B -- "Si" --> C["Classe normal"]
    B -- "No" --> D{"Dos objectes amb les mateixes dades son el mateix?"}
    D -- "No, te identitat propia" --> C
    D -- "Si, es un valor" --> E{"Necessito heretar o amagar camps?"}
    E -- "Si" --> C
    E -- "No" --> F["record"]

  1. Records a BiblioTech: DTO i objectes de valor

Dos usos concrets al projecte.

Objecte de valor: Fitxa. Una fitxa bibliogràfica de catàleg, sense identitat pròpia.

package com.nexussoftware.bibliotech.domini;

/** Fitxa bibliografica de cataleg. Objecte de valor immutable. */
public record Fitxa(String titol, String autor, int any) implements Comparable<Fitxa> {

    public Fitxa {
        titol = (titol == null || titol.isBlank()) ? "Sense titol" : titol.trim();
        autor = (autor == null || autor.isBlank()) ? "Desconegut"  : autor.trim();
        any   = (any < 1450 || any > 2100) ? 0 : any;
    }

    public Fitxa(String titol, String autor) { this(titol, autor, 0); }

    /** Fabrica des d'un llibre del cataleg. */
    public static Fitxa desDe(Llibre llibre) {
        return new Fitxa(llibre.getTitol(), llibre.getAutor(), llibre.getAnyPublicacio());
    }

    public boolean esClassic() { return any > 0 && any < 2000; }

    @Override
    public int compareTo(Fitxa altra) {
        return this.titol.compareToIgnoreCase(altra.titol);
    }
}

DTO de sortida: ResumSessio. Un objecte que transporta el resultat d'una sessió de devolucions des del servei fins a la presentació.

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.Gravetat;

/**
 * Resultat d'una sessio de devolucions.
 *
 * <p>Objecte de transferencia (DTO): agrupa dades calculades per lliurar-les
 * a la capa de presentacio sense exposar el domini.</p>
 */
public record ResumSessio(int devolucions,
                          double totalMultes,
                          int incidenciesGreus,
                          Gravetat gravetatMaxima) {

    public ResumSessio {
        devolucions      = Math.max(0, devolucions);
        totalMultes      = Math.max(0.0, totalMultes);
        incidenciesGreus = Math.max(0, incidenciesGreus);
        gravetatMaxima   = (gravetatMaxima == null) ? Gravetat.SENSE_RETARD : gravetatMaxima;
    }

    /** Sessio buida: punt de partida de l'acumulacio. */
    public static ResumSessio buit() {
        return new ResumSessio(0, 0.0, 0, Gravetat.SENSE_RETARD);
    }

    /** Retorna un resum NOU amb una devolucio mes. Estil immutable. */
    public ResumSessio mes(double multa, Gravetat gravetat) {
        return new ResumSessio(
                devolucions + 1,
                totalMultes + multa,
                incidenciesGreus + (gravetat == Gravetat.GREU ? 1 : 0),
                gravetat.esMesGreuQue(gravetatMaxima) ? gravetat : gravetatMaxima);
    }

    public double multaMitjana() {
        return devolucions == 0 ? 0.0 : totalMultes / devolucions;
    }
}

I l'ús conjunt, amb l'enum i el record treballant plegats:

Prestec[] retornats = { p1, p2, p3, p4 };
int[]     dies      = { 20, 40, 16, 12 };

ResumSessio resum = ResumSessio.buit();

for (int i = 0; i < retornats.length; i++) {
    double   multa    = retornats[i].registrarDevolucio(dies[i]);
    Gravetat gravetat = retornats[i].classificarGravetat();
    resum = resum.mes(multa, gravetat);      // es reassigna: cada 'mes' en crea un de nou
}

System.out.println(resum);
System.out.printf("Multa mitjana: %.2f EUR%n", resum.multaMitjana());
System.out.println("Accio: " + resum.gravetatMaxima().accioRecomanada());
ResumSessio[devolucions=4, totalMultes=15.30, incidenciesGreus=2, gravetatMaxima=GREU]
Multa mitjana: 3,83 EUR
Accio: Escalar al responsable i bloquejar nous prestecs.

Fixa't en el toString() que no has escrit, en l'equals que seria correcte si el necessitessis, i en l'última línia: gravetatMaxima() retorna un Gravetat, i sobre ell es crida directament accioRecomanada(). Un enum que porta el seu comportament i un record que porta les seves dades.

  1. Taula comparativa: classe, record i enum

Dimensió Classe record enum
Nombre d'instàncies Il·limitat Il·limitat Fix: una per constant
Estat mutable No No hauria
Constructor Escrit a mà Canònic generat + compacte Implícitament private
equals/hashCode/toString A mà Generats Heretats d'Enum, equals és final
Accessors getXxx() a mà xxx() generats A mà
Pot estendre classes No No
Pot ser estès Sí (si no és final) No No (llevat de cos per constant)
Pot implementar interfícies
Camps d'instància propis No (només components)
Comparació equals equals ==
Exhaustivitat en switch No No
Fes-lo servir per a Entitats amb identitat i estat Valors immutables, DTO Conjunts tancats de valors
A BiblioTech Material, Prestec, Empleat Fitxa, ResumSessio Gravetat, TipusMaterial

  1. Tancament del mòdul: estat de BiblioTech

Aplica els enums al projecte i el mòdul queda tancat. Prestec.Incidencia, que a 04-03 desava la gravetat com a String, passa a fer servir el tipus:

public static class Incidencia {

    private final int      dia;
    private final String   motiu;
    private final Gravetat gravetat;          // abans: String

    public Incidencia(int dia, String motiu, Gravetat gravetat) {
        this.dia      = Math.max(0, dia);
        this.motiu    = (motiu == null || motiu.isBlank())
                        ? "Sense especificar" : motiu.trim();
        this.gravetat = (gravetat == null) ? Gravetat.LLEU : gravetat;
    }

    public int      getDia()      { return dia; }
    public String   getMotiu()    { return motiu; }
    public Gravetat getGravetat() { return gravetat; }
    public boolean  esGreu()      { return gravetat == Gravetat.GREU; }

    @Override
    public String toString() {
        return String.format("dia %d - %s [%s]", dia, motiu, gravetat);
    }
}

I aquest és l'estat complet del projecte en acabar el mòdul 4:

classDiagram
    class Prestable {
        <<interface>>
        +prestar() boolean
        +retornar() boolean
        +estaDisponible() boolean
        +getDiesPrestec() int
        +diesRestants(int) int
    }
    class Notificable {
        <<interface>>
        +getCanalAvis() String
        +generarAvis(int) String
    }
    class Material {
        <<abstract>>
        +calcularMulta(int) double
        +classificarGravetat(int) Gravetat
        +getTipus()* String
    }
    class Gravetat {
        <<enumeration>>
        SENSE_RETARD
        LLEU
        GREU
        +accioRecomanada() String
    }
    class TipusMaterial {
        <<enumeration>>
        LLIBRE
        REVISTA
        DVD
    }
    class Fitxa {
        <<record>>
        +titol() String
        +autor() String
    }
    class ResumSessio {
        <<record>>
        +totalMultes() double
    }
    Prestable <|.. Material
    Notificable <|.. Material
    Prestable <|.. SalaReunions
    Material <|-- Llibre
    Material <|-- Revista
    Material <|-- Dvd
    Prestec --> Material
    Prestec --> Empleat
    Prestec *-- Incidencia
    Incidencia --> Gravetat
    Material --> Gravetat

Inventari del projecte després del mòdul 4:

Element Tipus Membres públics destacats
Prestable interfície prestar, retornar, estaDisponible, getDiesPrestec; default diesRestants, estaVencut; static terminiValid, comptarDisponibles; TERMINI_MAXIM_DIES
Notificable interfície getCanalAvis, generarAvis; default avisUrgent, avisRutinari; private capcalera
Material classe abstracta abstract getTipus/getDiesPrestec/getTarifaDiaria; final calcularDiesRetard/calcularMulta/classificarGravetat; prestar, retornar, descriure, equals, hashCode, toString
Llibre, Revista, Dvd classes concretes Els seus tres mètodes obligatoris més els seus camps propis
SalaReunions classe Prestable sense ser Material
Empleat classe potPrendrePrestat, registrarPrestec, registrarDevolucio, getInicials, getHistorial
Empleat.Historial imbricada estàtica Comptadors acumulats
Prestec classe registrarDevolucio, calcularMulta, classificarGravetat, anotarIncidencia, getIncidencies
Prestec.Incidencia imbricada estàtica getDia, getMotiu, getGravetat, esGreu
Gravetat enum SENSE_RETARD, LLEU, GREU; getNivell, accioRecomanada, requereixAvis, esMesGreuQue
TipusMaterial enum LLIBRE, REVISTA, DVD, AUDIO; getEtiqueta, getDiesPrestec, getTarifaDiaria, multaPer
Fitxa record titol(), autor(), any(), esClassic, desDe(Llibre)
ResumSessio record devolucions(), totalMultes(), gravetatMaxima(), mes, multaMitjana, buit()
ReglaTarifa, FiltreMaterial interfícies funcionals Un mètode abstracte cadascuna
Cataleg, GestorPrestecs serveis Reben Predicate, Function, Consumer, Comparator
RebutConsola, InformeMaterial presentació Template Method a InformeMaterial

Errors Habituals i Consells

Descuidar el punt i coma després de l'última constant. Si l'enum té camps o mètodes, la llista de constants acaba en ;. És l'error de compilació número u amb enums.

Dependre d'ordinal(). Reordenar les constants canvia tots els ordinals en silenci. Fes servir un camp explícit.

Desar ordinal() en un fitxer o base de dades. Pitjor encara: corromp dades ja desades. Desa name(), que és estable, o un codi propi.

Posar default en un switch d'expressió sobre un enum. Anul·la la comprovació d'exhaustivitat, que és el principal avantatge. Omet-lo.

Qualificar la constant dins del case. S'escriu case LLEU ->, no case Gravetat.LLEU ->. La segona forma no compila.

Fer servir equals amb enums. Funciona, però == és més segur (davant de null), més ràpid i detecta comparacions entre tipus diferents en compilació.

Escriure getTitol() en un record. L'accessor generat es diu titol(). Si escrius getTitol(), estàs afegint un mètode, no sobreescrivint res.

Posar la llista de paràmetres al constructor compacte. public Fitxa(String titol, ...) { és el constructor canònic explícit, no el compacte; llavors sí que has d'assignar els camps a mà.

Intentar declarar un camp d'instància en un record. No compila. El seu estat són exactament els seus components. Els static sí que es permeten.

Fer servir un record per a una entitat amb identitat. Un Empleat no és un valor: dos empleats amb el mateix nom no són el mateix empleat. La seva igualtat va per identificador, no per tots els camps.

Consell: enum per a tot conjunt tancat. Estats, tipus, canals, nivells, rols, monedes, dies de la setmana. Si els valors possibles són fixos i coneguts, és un enum. Convertir-los després costa molt més que començar bé.

Consell: record per a tot allò que sigui un valor. Coordenades, quantitats amb divisa, rangs, resultats de càlcul, DTO entre capes, claus compostes. Estalvies codi i elimines una font de bugs, perquè equals i hashCode generats són correctes per construcció.

Consell: combina'ls. Un record el component del qual és un enum és una parella extraordinàriament expressiva, com ara ResumSessio(..., Gravetat gravetatMaxima).

Exercicis

Els exercicis 2 i 3 s'apuntalen en els tipus de l'exercici 1, així que convé fer-los en ordre.

Exercici 1: EstatPrestec

Crea un enum EstatPrestec amb les constants ACTIU, VENCUT, RETORNAT i PERDUT. Cadascuna ha de portar una etiqueta llegible i un booleà que indiqui si compta per al límit de préstecs de l'empleat. Afegeix un mètode abstract String descripcio() implementat per cada constant i un switch d'expressió que retorni l'acció a fer, sense default. Comprova què passa en afegir una cinquena constant.

Exercici 2: record LiniaCataleg

Crea un record LiniaCataleg(String referencia, String titol, TipusMaterial tipus, boolean disponible) amb un constructor compacte que validi, un mètode estàtic desDe(Material), un mètode etiqueta() que retorni una línia formatada i la implementació de Comparable<LiniaCataleg> per títol. Genera un array de línies a partir del catàleg i ordena'l.

Exercici 3: informe amb enum i record

Escriu un mètode que recorri un array de préstecs i retorni un record InformeGravetat(int senseRetard, int lleus, int greus, double totalMultes). Fes servir un switch d'expressió sobre Gravetat sense default i l'estil immutable (un mètode mes(...) que retorni una còpia nova). Imprimeix l'informe i l'acció recomanada del nivell més alt detectat.

Solucions

Solució 1

package com.nexussoftware.bibliotech.domini;

/** Estat en que es troba un prestec. */
public enum EstatPrestec {

    ACTIU("Actiu", true) {
        @Override public String descripcio() {
            return "El material esta en poder de l'empleat i dins de termini.";
        }
    },

    VENCUT("Vencut", true) {
        @Override public String descripcio() {
            return "El termini s'ha superat i s'acumula multa diaria.";
        }
    },

    RETORNAT("Retornat", false) {
        @Override public String descripcio() {
            return "El material ha tornat al cataleg i esta disponible.";
        }
    },

    PERDUT("Perdut", false) {
        @Override public String descripcio() {
            return "El material es dona per perdut; s'aplica la multa maxima.";
        }
    };      // <-- punt i coma OBLIGATORI

    private final String  etiqueta;
    private final boolean comptaPerAlLimit;

    EstatPrestec(String etiqueta, boolean comptaPerAlLimit) {
        this.etiqueta         = etiqueta;
        this.comptaPerAlLimit = comptaPerAlLimit;
    }

    public String  getEtiqueta()      { return etiqueta; }
    public boolean comptaPerAlLimit() { return comptaPerAlLimit; }

    /** Cada constant l'HA d'implementar. */
    public abstract String descripcio();

    @Override public String toString() { return etiqueta; }

    /** Accio a fer. switch d'EXPRESSIO sense default: exhaustivitat garantida. */
    public String accio() {
        return switch (this) {
            case ACTIU    -> "Cap. Revisar al venciment.";
            case VENCUT   -> "Enviar avis i calcular multa.";
            case RETORNAT -> "Arxivar el prestec i alliberar el material.";
            case PERDUT   -> "Aplicar multa maxima i donar de baixa el material.";
        };
    }

    public static void main(String[] args) {
        System.out.printf("%-10s %-8s %s%n", "ESTAT", "LIMIT", "ACCIO");
        for (EstatPrestec e : EstatPrestec.values()) {
            System.out.printf("%-10s %-8b %s%n", e, e.comptaPerAlLimit(), e.accio());
        }
        System.out.println();
        System.out.println("Detall de VENCUT: " + EstatPrestec.VENCUT.descripcio());
        System.out.println("Comparacio amb ==: "
                           + (EstatPrestec.valueOf("VENCUT") == EstatPrestec.VENCUT));
    }
}
ESTAT      LIMIT    ACCIO
Actiu      true     Cap. Revisar al venciment.
Vencut     true     Enviar avis i calcular multa.
Retornat   false    Arxivar el prestec i alliberar el material.
Perdut     false    Aplicar multa maxima i donar de baixa el material.

Detall de VENCUT: El termini s'ha superat i s'acumula multa diaria.
Comparacio amb ==: true

Què passa en afegir una cinquena constant. Si afegeixes RENOVAT("Renovat", true), el compilador produeix dos errors, i tots dos són exactament el que vols:

error: RENOVAT is not abstract and does not override abstract method descripcio()
error: the switch expression does not cover all possible input values

El primer t'obliga a descriure l'estat nou; el segon, a decidir quina acció li correspon. Cap dels dos existiria amb cadenes: "RENOVAT" hauria caigut en un default silenciós i el sistema hauria fet alguna cosa incorrecta sense avisar. Aquesta és la raó número u per fer servir enums al domini.

Solució 2

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.*;
import java.util.Arrays;
import java.util.Comparator;

/** Linia de cataleg a punt per mostrar. Objecte de valor immutable. */
public record LiniaCataleg(String referencia,
                           String titol,
                           TipusMaterial tipus,
                           boolean disponible) implements Comparable<LiniaCataleg> {

    /** Constructor compacte: valida i normalitza ABANS d'assignar els camps. */
    public LiniaCataleg {
        referencia = (referencia == null || referencia.isBlank())
                     ? "SENSE-REF" : referencia.trim();
        titol      = (titol == null || titol.isBlank())
                     ? "Sense titol" : titol.trim();
        tipus      = (tipus == null) ? TipusMaterial.LLIBRE : tipus;
        // 'disponible' es boolean: no necessita validacio
    }

    /** Fabrica des del domini: tradueix la classe concreta a la constant de l'enum. */
    public static LiniaCataleg desDe(Material m) {
        TipusMaterial t = switch (m.getTipus()) {
            case "Llibre"      -> TipusMaterial.LLIBRE;
            case "Revista"     -> TipusMaterial.REVISTA;
            case "DVD"         -> TipusMaterial.DVD;
            case "Audiollibre" -> TipusMaterial.AUDIO;
            default            -> TipusMaterial.LLIBRE;
        };
        return new LiniaCataleg(m.getReferencia(), m.getTitol(), t, m.estaDisponible());
    }

    public String etiqueta() {
        return String.format("%-16s %-24s %-11s %s",
                             referencia, titol, tipus.getEtiqueta(),
                             disponible ? "LLIURE" : "PRESTAT");
    }

    @Override
    public int compareTo(LiniaCataleg altra) {
        return this.titol.compareToIgnoreCase(altra.titol);
    }

    public static void main(String[] args) {

        Material[] cataleg = {
            new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
            new Llibre("Java Eficac",        "Joshua Bloch",  "978-0000000001", 2018),
            new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
            new Llibre("Patrons de Disseny", "Erich Gamma",   "978-0000000002", 1994)
        };
        cataleg[1].prestar();

        // Transformar el domini en linies de presentacio
        LiniaCataleg[] linies = new LiniaCataleg[cataleg.length];
        for (int i = 0; i < cataleg.length; i++) {
            linies[i] = LiniaCataleg.desDe(cataleg[i]);
        }

        // Ordre natural (Comparable): per titol
        Arrays.sort(linies);
        System.out.println("--- Ordre natural (per titol) ---");
        for (LiniaCataleg l : linies) { System.out.println("  " + l.etiqueta()); }

        // Ordre alternatiu amb Comparator i referencies a metode (04-06)
        Arrays.sort(linies, Comparator.comparing(LiniaCataleg::tipus)
                                      .thenComparing(LiniaCataleg::titol));
        System.out.println("--- Per tipus i titol ---");
        for (LiniaCataleg l : linies) { System.out.println("  " + l.etiqueta()); }

        // equals i hashCode generats: funcionen sense escriure res
        LiniaCataleg a = new LiniaCataleg("DVD-0007", "Refactoritzacio en directe",
                                          TipusMaterial.DVD, true);
        LiniaCataleg b = LiniaCataleg.desDe(cataleg[0]);
        System.out.println("--- Igualtat per valor ---");
        System.out.println("  a.equals(b) = " + a.equals(b));
        System.out.println("  toString    = " + a);
    }
}
--- Ordre natural (per titol) ---
  978-0000000001   Java Eficac              Llibre      PRESTAT
  REV-2024-03      Java Magazine            Revista     LLIURE
  978-0000000002   Patrons de Disseny       Llibre      LLIURE
  DVD-0007         Refactoritzacio en directe DVD         LLIURE
--- Per tipus i titol ---
  978-0000000001   Java Eficac              Llibre      PRESTAT
  978-0000000002   Patrons de Disseny       Llibre      LLIURE
  REV-2024-03      Java Magazine            Revista     LLIURE
  DVD-0007         Refactoritzacio en directe DVD         LLIURE
--- Igualtat per valor ---
  a.equals(b) = true
  toString    = LiniaCataleg[referencia=DVD-0007, titol=Refactoritzacio en directe, tipus=DVD, disponible=true]

Tres observacions. Primera: l'ordre "per tipus i títol" col·loca els llibres abans que la revista i el DVD perquè Comparator.comparing sobre un enum fa servir el seu ordre natural, que és el de declaració (LLIBRE, REVISTA, DVD, AUDIO); és l'únic ús legítim de l'ordinal, i el fa el JDK per tu. Segona: a.equals(b) és true sense haver escrit ni una línia d'equals, perquè el record compara tots els seus components. Tercera: LiniaCataleg::tipus i LiniaCataleg::titol són referències als accessors generats, sense get.

Solució 3

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.Gravetat;
import com.nexussoftware.bibliotech.domini.Prestec;

public class AnalitzadorGravetat {

    /** Recompte de prestecs per gravetat. Objecte de valor immutable. */
    public record InformeGravetat(int senseRetard, int lleus, int greus, double totalMultes) {

        public InformeGravetat {
            senseRetard = Math.max(0, senseRetard);
            lleus       = Math.max(0, lleus);
            greus       = Math.max(0, greus);
            totalMultes = Math.max(0.0, totalMultes);
        }

        public static InformeGravetat buit() {
            return new InformeGravetat(0, 0, 0, 0.0);
        }

        /** Retorna un informe NOU amb un prestec mes. Estil immutable. */
        public InformeGravetat mes(Gravetat g, double multa) {
            // switch d'EXPRESSIO sense default: si s'afegeix una constant, no compila
            return switch (g) {
                case SENSE_RETARD -> new InformeGravetat(senseRetard + 1, lleus, greus,
                                                         totalMultes + multa);
                case LLEU         -> new InformeGravetat(senseRetard, lleus + 1, greus,
                                                         totalMultes + multa);
                case GREU         -> new InformeGravetat(senseRetard, lleus, greus + 1,
                                                         totalMultes + multa);
            };
        }

        public int total() { return senseRetard + lleus + greus; }

        /** Nivell maxim assolit, deduit del recompte. */
        public Gravetat gravetatMaxima() {
            if (greus > 0) { return Gravetat.GREU; }
            if (lleus > 0) { return Gravetat.LLEU; }
            return Gravetat.SENSE_RETARD;
        }

        public String informeText() {
            return String.format(
                    "Prestecs analitzats: %d%n"
                  + "  Sense retard: %d%n"
                  + "  Lleus:        %d%n"
                  + "  Greus:        %d%n"
                  + "  Multes:       %.2f EUR (mitjana %.2f EUR)%n"
                  + "  Nivell maxim: %s -> %s",
                    total(), senseRetard, lleus, greus, totalMultes,
                    total() == 0 ? 0.0 : totalMultes / total(),
                    gravetatMaxima(), gravetatMaxima().accioRecomanada());
        }
    }

    /** Analitza els prestecs indicats als dies transcorreguts corresponents. */
    public static InformeGravetat analitzar(Prestec[] prestecs, int[] dies) {
        InformeGravetat informe = InformeGravetat.buit();
        for (int i = 0; i < prestecs.length; i++) {
            double   multa    = prestecs[i].getMaterial().calcularMulta(dies[i]);
            Gravetat gravetat = prestecs[i].getMaterial().classificarGravetat(dies[i]);
            informe = informe.mes(gravetat, multa);      // cada 'mes' en crea un de nou
        }
        return informe;
    }
}
Empleat marta = new Empleat("Marta Ruiz",   "EMP-001");
Empleat diego = new Empleat("Diego Alonso", "EMP-002");

Prestec[] prestecs = {
    new Prestec(new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018), marta, 100),
    new Prestec(new Dvd("Refactoritzacio en directe", "DVD-0007", 95), diego, 100),
    new Prestec(new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"), marta, 100),
    new Prestec(new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994), diego, 100)
};
int[] dies = { 20, 25, 9, 10 };

System.out.println(AnalitzadorGravetat.analitzar(prestecs, dies).informeText());
Prestecs analitzats: 4
  Sense retard: 1
  Lleus:        2
  Greus:        1
  Multes:       12,45 EUR (mitjana 3,11 EUR)
  Nivell maxim: GREU -> Escalar al responsable i bloquejar nous prestecs.

Tres punts de disseny. Primer, el switch no porta default: si demà afegeixes MOLT_GREU a Gravetat, aquest mètode no compilarà i el compilador et portarà al punt exacte que cal revisar. Segon, mes(...) no modifica l'informe, en retorna un de nou; per això el bucle escriu informe = informe.mes(...). És l'estil immutable de 03-07, natural amb records. Tercer, gravetatMaxima().accioRecomanada() encadena l'enum amb el seu comportament per constant: l'informe no té ni un sol if sobre el nivell de gravetat.

Conclusió

Has tancat el mòdul amb les dues eines que converteixen dades soltes en tipus amb significat. Saps que un enum és un tipus els valors possibles del qual formen un conjunt tancat i conegut en compilació, i quins problemes resol enfront de les constants String o int: seguretat de tipus —Gravetat.GRAVISSIM no compila, "GRAVISSIM" sí—, autocompletat, comportament associat i, sobretot, exhaustivitat: un switch d'expressió sense default sobre un enum obliga el compilador a portar-te de la mà a cada punt del projecte que cal actualitzar quan hi afegeixes una constant. Coneixes els seus mètodes implícits —values(), valueOf(), name(), ordinal()— i per què ordinal() no ha d'aparèixer mai a la teva lògica ni a les teves dades desades: reordenar les constants ho canvia tot en silenci. Saps que amb enums == és la comparació correcta, més segura davant de null i capaç de detectar en compilació una comparació entre tipus diferents que equals amagaria.

Domines l'enum amb camps, constructor i mètodes, que substitueix una jerarquia sencera quan l'únic que varia són valors —i saps quan no l'ha de substituir, perquè a BiblioTech cada suport té camps propis—; i l'enum amb cos per constant, on cada valor implementa a la seva manera un mètode abstracte i el compilador exigeix que cap no es quedi sense implementar.

Saps que un record declara en una línia el que costava quaranta: genera el constructor canònic, un accessor per component amb el nom del component —titol(), no getTitol()—, i els tres mètodes d'Object que tanta feina van donar a 03-09, correctes per construcció. Fas servir el constructor compacte per validar sense repetir la llista de paràmetres ni assignar camps a mà, hi afegeixes constructors addicionals que deleguen en el canònic, mètodes propis, constants i interfícies. I en coneixes els límits i el criteri d'ús: un record no estén ni és estès, el seu estat són exactament els seus components, i la pregunta que decideix és sempre la mateixa: dos objectes amb les mateixes dades són el mateix objecte? Si sí, és un valor i és un record; si té identitat pròpia o canvia amb el temps —Empleat, Prestec—, és una classe.

BiblioTech, després del mòdul 4, és un projecte d'orientació a objectes complet. Material és una classe abstracta que no es pot instanciar, obliga cada suport a declarar el seu tipus, el seu termini i la seva tarifa, i aplica un calcularMulta final en forma de Template Method. Signa dues interfícies, Prestable i Notificable, que una SalaReunions pot signar sense entrar a la família. Les incidències són una classe imbricada estàtica immutable, i la gravetat ja no és una cadena sinó l'enum Gravetat, que a més porta l'acció recomanada de cada nivell. El catàleg s'ordena i es filtra amb comparadors, predicats i funcions que arriben com a paràmetres, de manera que criteris nous no toquen ni una línia del codi existent. I Fitxa i ResumSessio transporten dades entre capes com a valors immutables, sense ni una sola línia de codi repetitiu.

I tanmateix, mira de prop qualsevol de les classes que has escrit i veuràs el mateix pedaç repetit: Material[] cataleg = new Material[10], Incidencia[] ampliat = Arrays.copyOf(incidencies, incidencies.length + 1), un Prestec[] de mida fixa, una cerca de grups en bucle imbricat O(n²) perquè no hi ha manera de cercar per clau. Cada vegada que BiblioTech necessita desar moltes coses, recorre a un array de mida fixa que cal copiar sencer per afegir-hi un element, recórrer complet per trobar-ne un i retallar a mà per retornar un resultat. Ho has anat marcant com a provisional lliçó rere lliçó, i ja és el més urgent que li falta al projecte.

Al mòdul 5, Estructures de Dades i Col·leccions, això desapareix. Començaràs per dominar els arrays de debò —inclosos els multidimensionals i la classe Arrays— i entraràs al Framework de Col·leccions de Java: ArrayList per a llistes que creixen soles, LinkedList per a insercions ràpides, HashMap per cercar per clau en temps constant (i allà veuràs per fi per què equals i hashCode havien d'anar sempre junts), HashSet per a conjunts sense duplicats, cues, piles i Deque, i els algorismes d'ordenació i cerca que aplicaran tots els comparadors que has escrit en aquest mòdul. Els arrays provisionals de BiblioTech es convertiran en col·leccions, i aquell informePerTipus de vint línies amb bucles imbricats es quedarà en tres.

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