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
- El problema: constants com a
Stringoint - Què és un
enum - Mètodes implícits:
values,valueOf,name,ordinal - Comparació d'enums:
==enfront d'equals enumamb camps, constructor i mètodesenumamb cos per constantenumenswitchi exhaustivitat- El singleton amb
enum - Registres: què és un
record - Què genera un
recordautomàticament - Constructor compacte i validació
- Constructors addicionals i mètodes propis
- Què NO pot fer un
recordi quan no fer-lo servir - Records a BiblioTech: DTO i objectes de valor
- Taula comparativa: classe,
recordienum - Tancament del mòdul: estat de BiblioTech
- Errors Habituals i Consells
- Exercicis
- El problema: constants com a
String o int
String o intAquest é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:
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 dominiAquest antipatró té nom a la literatura: int enum pattern. Java 5 el va resoldre d'una vegada.
- Què és un
enum
enumUn 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 constantSota 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ícitamentprivate. - 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 COMPILAL'error d'escriptura ha passat de ser una fallada silenciosa en producció a un error de compilació.
- Mètodes implícits:
values, valueOf, name, ordinal
values, valueOf, name, ordinalTot 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());
}// 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 execuciovalueOf 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; }
}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.
- Comparació d'enums:
== enfront d'equals
== enfront d'equalsAmb 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 innecessariQuatre raons, que recuperen el que vas aprendre a 01-05 i 03-09:
- 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ésfinali està implementat exactament comthis == other. ==és segur davant denull.a == nulldonafalse;a.equals(b)ambanul llançaNullPointerException.==dona seguretat de tipus. Comparar dos enums de tipus diferents amb==no compila; ambequalscompila i retornafalsesempre, amagant l'error.- É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
enum amb camps, constructor i mètodes
enum amb camps, constructor i mètodesAquí é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
privatei 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: Llibre té autor i anyPublicacio, Revista té numero i periodicitat, Dvd té duracioMinuts. 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.
enum amb cos per constant
enum amb cos per constantUna 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()); // ...GravetatLa 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.
enum en switch i exhaustivitat
enum en switch i exhaustivitatEls 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:
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
switchd'expressió sobre un enum, omet eldefaultsempre 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.
- El singleton amb
enum
enumUna 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");
- Registres: què és un
record
recordCanviem 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:
Una línia. I és completament equivalent, amb els tres mètodes correctament implementats.
La declaració es llegeix així:
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.
- Què genera un
record automàticament
record automàticamentD'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 diferentsFixa'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ícitamentfinal. 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'Objectdirectament. Per això unrecordno pot estendre cap altra classe (apartat 13).
- 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
}
}Tres regles del constructor compacte:
- No porta llista de paràmetres: s'escriu
public Fitxa {, nopublic Fitxa(String titol, ...) {. - 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. - 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.
- 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 + ")" : "");
}
}
- Què NO pot fer un
record i quan no fer-lo servir
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
EmpleatambEMP-001continua sent el mateix empleat encara que canviï de nom; duesFitxaamb 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
Presteccanvia de disponibilitat, acumula incidències i registra devolucions. No és un record. - Quan vols amagar la representació interna. Un
recordexposa tots els seus components. Si necessites encapsular la representació (03-07), fes servir una classe. - Quan necessites heretar. Un
recordno pot estendre ni ser estès. - Quan l'API pública ha de seguir el conveni
getXxxper 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"]
- 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.
- Taula comparativa: classe,
record i enum
record i enum| Dimensió | Classe | record |
enum |
|---|---|---|---|
| Nombre d'instàncies | Il·limitat | Il·limitat | Fix: una per constant |
| Estat mutable | Sí | 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 | Sí | No | No |
| Pot ser estès | Sí (si no és final) |
No | No (llevat de cos per constant) |
| Pot implementar interfícies | Sí | Sí | Sí |
| Camps d'instància propis | Sí | No (només components) | Sí |
| Comparació | equals |
equals |
== |
Exhaustivitat en switch |
No | No | Sí |
| 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 |
- 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
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
