La lliçó anterior va acabar amb un cap solt incòmode: new Prestec() produeix un préstec sense llibre, sense empleat i amb zero dies transcorreguts, i per deixar-lo utilitzable cal omplir-ne els camps un a un des de fora, confiant a no oblidar-ne cap. Aquest objecte a mig fer és una bomba de rellotgeria: entre el new i l'última assignació hi ha un interval en què l'objecte és invàlid, i si algú el fa servir aleshores, obté un NullPointerException o, pitjor, un resultat silenciosament incorrecte. Els constructors resolen exactament aquest problema. Un constructor és el bloc de codi que s'executa en crear un objecte, i la seva missió és garantir que cap objecte no neixi en un estat invàlid. En aquesta lliçó aprendràs a escriure'ls, a sobrecarregar-los, a encadenar-los, a controlar l'ordre exacte en què s'inicialitza tot i a validar les dades d'entrada. En acabar, Llibre, Empleat i Prestec seran objectes que neixen complets i coherents.
Contingut
- Què és un constructor
- Constructor enfront de mètode: set diferències
- El constructor per defecte i quan desapareix
- Sobrecàrrega de constructors
- Encadenament amb
this(...)i les seves regles - Ordre d'inicialització, pas a pas
- Camps
finali la seva inicialització obligatòria - El constructor telescòpic i el patró Builder
- Validar al constructor
- El constructor de còpia
- Blocs
staticd'inicialització - BiblioTech:
Llibre,EmpleatiPrestecamb constructors complets - Errors Habituals i Consells
- Exercicis
- Què és un constructor
Un constructor és un bloc amb el mateix nom que la classe i sense tipus de retorn, que s'executa automàticament en crear un objecte amb new:
public class Llibre {
public String titol;
public String autor;
public String isbn;
/** Constructor: s'executa en fer new Llibre(...). */
public Llibre(String titol, String autor, String isbn) {
this.titol = titol;
this.autor = autor;
this.isbn = isbn;
}
}I des d'aquest moment, crear un llibre és una sola línia que no es pot quedar a mitges:
Compara-ho amb el que feies fins ara:
// ABANS: quatre linies, i res no t'obliga a escriure-les totes quatre
Llibre javaEficac = new Llibre();
javaEficac.titol = "Java Eficac";
javaEficac.autor = "Joshua Bloch";
javaEficac.isbn = "978-0000000001";La diferència no és de brevetat, és de garanties. Amb el constructor, el compilador exigeix les tres dades: és literalment impossible crear un Llibre sense títol. Aquest principi té nom i t'acompanyarà tota la carrera: fer que els estats invàlids siguin irrepresentables.
Fixa't també en l'ús de this (lliçó 03-03): els paràmetres es diuen igual que els camps —convenció universal en Java— i this.titol distingeix el camp del paràmetre que l'enfosqueix.
- Constructor enfront de mètode: set diferències
| Aspecte | Constructor | Mètode |
|---|---|---|
| Nom | Idèntic al de la classe, amb majúscula inicial | Qualsevol, en lowerCamelCase |
| Tipus de retorn | Cap (ni tan sols void) |
Obligatori (void o un tipus) |
| Quan s'executa | Només en crear l'objecte amb new |
Quan se'l crida explícitament |
| S'hereta | No (03-05) | Sí |
Pot ser static |
No | Sí |
return |
Només return; per sortir abans |
return valor; segons el tipus |
| Si no l'escrius | Java te'n dona un per defecte | No apareix res |
L'error més comú de tots és aquest:
public class Llibre {
public void Llibre(String titol) { // NO es un constructor!
this.titol = titol;
}
}Posar void converteix el constructor en un mètode normal que casualment es diu Llibre. Compila sense protestar, new Llibre("...") deixa de compilar i el programador es torna boig buscant-ne el motiu. Regla: un constructor no porta tipus de retorn, mai.
- El constructor per defecte i quan desapareix
Si una classe no declara cap constructor, Java n'hi afegeix un automàticament: el constructor per defecte, sense paràmetres i amb el cos buit.
public class Llibre { } // Java hi afegeix: public Llibre() { }
Llibre l = new Llibre(); // funcionaPerò tan bon punt declares qualsevol constructor, el regal desapareix:
public class Llibre {
public Llibre(String titol) { this.titol = titol; }
}
Llibre l = new Llibre(); // ERROR de compilacioerror: constructor Llibre in class Llibre cannot be applied to given types; required: String found: no arguments
És un comportament deliberat: si has decidit que un llibre necessita títol, Java no permetrà a ningú saltar-s'ho. Si a més vols permetre la creació sense arguments, l'has de declarar explícitament:
- Sobrecàrrega de constructors
Els constructors se sobrecarreguen igual que els mètodes: mateix nom, diferent llista de paràmetres. Serveix per oferir diverses formes de crear el mateix objecte.
public class Llibre {
public String titol;
public String autor;
public String isbn;
public int anyPublicacio;
public boolean disponible;
/** Constructor complet. */
public Llibre(String titol, String autor, String isbn,
int anyPublicacio, boolean disponible) {
this.titol = titol;
this.autor = autor;
this.isbn = isbn;
this.anyPublicacio = anyPublicacio;
this.disponible = disponible;
}
/** Constructor habitual: un llibre nou entra disponible. */
public Llibre(String titol, String autor, String isbn, int anyPublicacio) {
this(titol, autor, isbn, anyPublicacio, true);
}
/** Constructor minim: per a altes rapides del cataleg. */
public Llibre(String titol, String isbn) {
this(titol, "Desconegut", isbn, 0, true);
}
}Tres formes de crear un llibre, totes coherents:
Llibre a = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018, true);
Llibre b = new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994);
Llibre c = new Llibre("Refactoritzacio", "978-0000000003");
- Encadenament amb
this(...) i les seves regles
this(...) i les seves reglesA l'exemple anterior apareix this(...): la crida d'un constructor a un altre constructor de la mateixa classe. És el mecanisme que evita duplicar codi d'inicialització.
flowchart LR
A["new Llibre(titol, isbn)"] --> B["Llibre(String, String)"]
B -->|"this(...)"| C["Llibre(String, String, String, int, boolean)
constructor complet:
assigna tots els camps"]
El patró professional és tenir un únic constructor que realment assigna (anomenat de vegades constructor canònic) i que tots els altres hi desemboquin mitjançant this(...). Així la validació s'escriu una sola vegada.
Regles estrictes que imposa el compilador:
this(...)ha de ser la primera sentència del constructor. Ni unSystem.out.printlnabans.- No hi pot haver alhora
this(...)isuper(...)(lliçó 03-05): només una de les dues, i sempre primera. - No es poden formar cicles: si
A()cridaB()iB()cridaA(), el compilador ho detecta i falla amb "recursive constructor invocation".
public Llibre(String titol, String isbn) {
System.out.println("Creant llibre..."); // ERROR si va abans
this(titol, "Desconegut", isbn, 0, true);
}
- Ordre d'inicialització, pas a pas
Quan executes new, passen diverses coses en un ordre fix que convé conèixer, perquè explica comportaments aparentment màgics. Les etapes són:
- Es reserva memòria i tots els camps prenen el seu valor per defecte (
0,false,null). - S'executen els inicialitzadors de camp i els blocs d'inicialització d'instància, en l'ordre textual en què apareixen al fitxer.
- S'executa el cos del constructor.
Un exemple traçat que ho demostra:
public class DemoOrdre {
public int a = mostrar("1. inicialitzador del camp a");
{
mostrar("2. bloc d'inicialitzacio d'instancia");
}
public int b = mostrar("3. inicialitzador del camp b");
public DemoOrdre() {
mostrar("4. cos del constructor");
}
private static int mostrar(String missatge) {
System.out.println(missatge);
return 0;
}
public static void main(String[] args) {
new DemoOrdre();
}
}Sortida:
1. inicialitzador del camp a 2. bloc d'inicialitzacio d'instancia 3. inicialitzador del camp b 4. cos del constructor
Dues conclusions importants:
- Els inicialitzadors i blocs s'executen abans que el cos del constructor, sempre, i en ordre de dalt a baix. Si tens tres constructors sobrecarregats, aquest codi comú s'executa als tres.
- Els blocs d'inicialització d'instància (les claus soltes) serveixen per a codi comú a tots els constructors, però a la pràctica es fan servir poc: és més clar un constructor canònic al qual els altres encadenen amb
this(...).
- Camps
final i la seva inicialització obligatòria
final i la seva inicialització obligatòriaJa saps que final impedeix reassignar una variable (mòdul 1). Aplicat a un camp, significa que el seu valor es fixa una sola vegada i no canvia durant la vida de l'objecte:
public class Llibre {
public final String isbn; // no te inicialitzador
public Llibre(String isbn) {
this.isbn = isbn; // obligatori assignar-lo aqui
}
}Un camp final ha de quedar assignat exactament una vegada en acabar tot constructor, ja sigui a la seva declaració, en un bloc d'inicialització o al cos del constructor. El compilador en verifica els tres camins:
public class Llibre {
public final String isbn;
public Llibre() { } // ERROR: variable isbn might not have been initialized
}public final String isbn = "000";
public Llibre(String isbn) {
this.isbn = isbn; // ERROR: cannot assign a value to final variable isbn
}Els camps final són una eina de disseny de primer ordre: expressen en el codi mateix que una dada és identitat de l'objecte i no una cosa que es pugui canviar. L'ISBN d'un llibre no canvia mai; el títol tampoc. El que sí que canvia és disponible. Aquesta distinció, marcada amb final, documenta el model millor que qualsevol comentari, i prepara el terreny per a la immutabilitat que estudiaràs a 03-07.
- El constructor telescòpic i el patró Builder
Quan una classe té molts camps opcionals, la sobrecàrrega degenera en el que es coneix com a constructor telescòpic:
public Llibre(String titol) { ... }
public Llibre(String titol, String autor) { ... }
public Llibre(String titol, String autor, String isbn) { ... }
public Llibre(String titol, String autor, String isbn, int any) { ... }
public Llibre(String titol, String autor, String isbn, int any, boolean disp) { ... }
public Llibre(String titol, String autor, String isbn, int any, boolean disp, String editorial) { ... }Els problemes són reals i greus:
- Il·legibilitat en la crida. Què significa
new Llibre("Java Eficac", "Bloch", "978-...", 2018, true, "Anaya")? Cal obrir la classe per saber-ho. - Errors silenciosos per ordre. Si intercanvies dos paràmetres del mateix tipus —
titoliisbn, tots dosString—, compila perfectament i l'error apareix en producció. - Explosió combinatòria. Amb sis camps opcionals caldrien desenes de constructors.
Una millora parcial i barata: anomenar les variables abans de cridar.
String titol = "Java Eficac";
String isbn = "978-0000000001";
Llibre l = new Llibre(titol, "Joshua Bloch", isbn, 2018, true);La solució completa és el patró Builder, que permet escriure una cosa com aquesta:
Llibre llibre = new Llibre.Builder("Java Eficac", "978-0000000001")
.autor("Joshua Bloch")
.anyPublicacio(2018)
.disponible(true)
.build();Cada dada queda anomenada a la crida, l'ordre deixa d'importar i els opcionals s'ometen. Requereix classes internes estàtiques, que s'estudien a 04-03, així que el patró es desenvolupa a la lliçó 12-02 (Patrons de Disseny). Per ara, queda't amb la regla pràctica: si necessites més de quatre o cinc paràmetres en un constructor, és senyal que falta agrupar dades en objectes o que necessites un Builder.
- Validar al constructor
Si el constructor és el guardià de l'entrada a l'objecte, ha de rebutjar les dades absurdes. Un any de publicació de -500, una tarifa negativa o un ISBN buit no han de poder entrar.
Nota important sobre el mecanisme. La forma professional de rebutjar un argument invàlid és llançar una excepció (
throw new IllegalArgumentException(...)), cosa que impedeix que l'objecte arribi a existir. Però les excepcions són el mòdul 6 i encara no les hem estudiades. Per això, durant aquest mòdul, la validació consistirà a avisar per consola i aplicar un valor per defecte segur. És una solució provisional i deliberadament imperfecta: recorda-ho, perquè al mòdul 6 tornarem a aquestes mateixes classes per fer-ho bé.
public Llibre(String titol, String autor, String isbn,
int anyPublicacio, boolean disponible) {
// --- Validacio del titol ---
if (titol == null || titol.isBlank()) {
System.out.println("AVIS: titol buit. Es fara servir 'Sense titol'.");
this.titol = "Sense titol";
} else {
this.titol = titol.trim();
}
// --- Validacio de l'autor ---
this.autor = (autor == null || autor.isBlank()) ? "Desconegut" : autor.trim();
// --- Validacio de l'ISBN ---
if (isbn == null || isbn.isBlank()) {
System.out.println("AVIS: ISBN buit. Es fara servir '000-0000000000'.");
this.isbn = "000-0000000000";
} else {
this.isbn = isbn.trim();
}
// --- Validacio de l'any ---
if (anyPublicacio < 1450 || anyPublicacio > 2100) {
System.out.println("AVIS: any " + anyPublicacio + " fora de rang. Es fara servir 0.");
this.anyPublicacio = 0;
} else {
this.anyPublicacio = anyPublicacio;
}
this.disponible = disponible;
llibresCreats++;
}Observa tres detalls: isBlank() (Java 11+) és millor que isEmpty() perquè també detecta cadenes de només espais; la comprovació de null va abans que la de contingut, aprofitant el curtcircuit de || que vas estudiar al mòdul 1; i trim() normalitza l'entrada, evitant que "Java Eficac " i "Java Eficac" es tractin com a títols diferents.
I aquí hi ha l'altre gran benefici: el comptador llibresCreats++ ara viu dins del constructor, no repartit per main. És impossible crear un llibre sense comptar-lo.
- El constructor de còpia
Un constructor de còpia rep un objecte de la mateixa classe i en crea un de nou amb els seus mateixos valors. És la solució al problema de l'aliasing de la lliçó 03-02: quan de debò vols un objecte independent.
/**
* Constructor de copia: crea un exemplar nou amb les mateixes dades
* bibliografiques que un altre. Util per registrar duplicats al cataleg.
*/
public Llibre(Llibre altre) {
this(altre.titol, altre.autor, altre.isbn, altre.anyPublicacio, altre.disponible);
}Ús:
Llibre original = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Llibre segonExemplar = new Llibre(original);
segonExemplar.disponible = false;
System.out.println(original.disponible); // true <-- independent
System.out.println(original == segonExemplar); // false <-- objectes diferentsCompara-ho amb Llibre alies = original;, que no copiava res. Aquí sí que hi ha dos objectes al heap.
Un matís que reapareixerà a 03-07: aquesta còpia és superficial (shallow). Copia els valors dels camps; si algun camp fos una referència a un objecte mutable, totes dues còpies compartirien aquest objecte intern. Amb String i int no hi ha problema, perquè són immutables o primitius.
- Blocs
static d'inicialització
static d'inicialitzacióIgual que existeixen blocs d'inicialització d'instància, existeixen blocs static, que s'executen una sola vegada, quan la JVM carrega la classe, abans que es creï cap objecte:
public class Prestec {
public static final int DIES_PRESTEC = 15;
public static final double TARIFA_DIARIA = 0.25;
public static final double MULTA_MAXIMA = 20.0;
public static final int LLINDAR_LLEU = 7;
/** Dies de retard a partir dels quals s'assoleix el sostre de multa. */
public static final int DIES_FINS_AL_SOSTRE;
static {
// Es calcula una sola vegada, en carregar la classe.
DIES_FINS_AL_SOSTRE = (int) Math.ceil(MULTA_MAXIMA / TARIFA_DIARIA);
System.out.println("[Prestec] Regles carregades. Sostre als "
+ DIES_FINS_AL_SOSTRE + " dies de retard.");
}
}En executar qualsevol cosa que toqui Prestec, veuràs un sol cop:
L'ordre global, resumit en una taula:
| Moment | Què s'executa | Quantes vegades |
|---|---|---|
| Càrrega de la classe | Inicialitzadors static i blocs static, en ordre textual |
Una per execució |
new |
Camps per defecte → inicialitzadors d'instància i blocs { } → cos del constructor |
Una per objecte |
Els blocs static es fan servir sobretot per inicialitzar constants calculades o carregar configuració. Fes-los servir amb moderació: codi que s'executa "sol" és difícil de depurar.
- BiblioTech:
Llibre, Empleat i Prestec amb constructors complets
Llibre, Empleat i Prestec amb constructors completsApliquem tot l'anterior a les tres classes del domini.
Llibre
package com.nexussoftware.bibliotech.domini;
/**
* Llibre del cataleg tecnic de Nexus Software.
* Les dades bibliografiques son immutables (final); nomes canvia la disponibilitat.
*/
public class Llibre {
public static int llibresCreats = 0;
public final String titol;
public final String autor;
public final String isbn;
public final int anyPublicacio;
public boolean disponible; // unic camp mutable
/** Constructor canonic: unic que assigna i valida. */
public Llibre(String titol, String autor, String isbn,
int anyPublicacio, boolean disponible) {
this.titol = (titol == null || titol.isBlank()) ? "Sense titol" : titol.trim();
this.autor = (autor == null || autor.isBlank()) ? "Desconegut" : autor.trim();
if (isbn == null || isbn.isBlank()) {
System.out.println("AVIS: ISBN buit a '" + this.titol + "'.");
this.isbn = "000-0000000000";
} else {
this.isbn = isbn.trim();
}
if (anyPublicacio < 1450 || anyPublicacio > 2100) {
System.out.println("AVIS: any invalid (" + anyPublicacio + ") a '"
+ this.titol + "'. Es registra com a 0.");
this.anyPublicacio = 0;
} else {
this.anyPublicacio = anyPublicacio;
}
this.disponible = disponible;
llibresCreats++;
}
/** Alta habitual: un llibre nou entra disponible. */
public Llibre(String titol, String autor, String isbn, int anyPublicacio) {
this(titol, autor, isbn, anyPublicacio, true);
}
/** Constructor de copia: exemplar nou amb les mateixes dades. */
public Llibre(Llibre altre) {
this(altre.titol, altre.autor, altre.isbn, altre.anyPublicacio, altre.disponible);
}
public boolean estaDisponible() { return disponible; }
public void prestar() {
if (!disponible) {
System.out.println("AVIS: '" + titol + "' ja estava prestat.");
return;
}
disponible = false;
}
public void retornar() {
if (disponible) {
System.out.println("AVIS: '" + titol + "' ja estava disponible.");
return;
}
disponible = true;
}
public String descriure() {
return titol + " - " + autor + " (" + anyPublicacio + "), ISBN " + isbn;
}
}Empleat
package com.nexussoftware.bibliotech.domini;
/** Empleat de Nexus Software autoritzat a prendre llibres en prestec. */
public class Empleat {
public static final int MAX_PRESTECS_SIMULTANIS = 3;
public static int empleatsRegistrats = 0;
public final String nom;
public final String identificador;
public int prestecsAcumulats;
public Empleat(String nom, String identificador, int prestecsAcumulats) {
this.nom = (nom == null || nom.isBlank()) ? "Empleat sense nom"
: nom.trim();
if (identificador == null || !identificador.startsWith("EMP-")) {
System.out.println("AVIS: identificador invalid per a " + this.nom
+ ". S'assigna EMP-000.");
this.identificador = "EMP-000";
} else {
this.identificador = identificador.trim();
}
if (prestecsAcumulats < 0) {
System.out.println("AVIS: prestecs negatius a " + this.nom + ". Es fa servir 0.");
this.prestecsAcumulats = 0;
} else {
this.prestecsAcumulats = prestecsAcumulats;
}
empleatsRegistrats++;
}
/** Alta habitual: empleat nou, sense prestecs. */
public Empleat(String nom, String identificador) {
this(nom, identificador, 0);
}
public boolean potPrendrePrestat() {
return prestecsAcumulats < MAX_PRESTECS_SIMULTANIS;
}
public void registrarPrestec() {
if (!potPrendrePrestat()) {
System.out.printf("AVIS: %s ja te %d prestecs (maxim %d).%n",
nom, prestecsAcumulats, MAX_PRESTECS_SIMULTANIS);
return;
}
prestecsAcumulats++;
}
public void registrarDevolucio() {
prestecsAcumulats = Math.max(0, prestecsAcumulats - 1);
}
}Prestec
Aquí apareix la novetat promesa: el préstec calcula el seu propi dia de venciment en néixer, a partir de DIES_PRESTEC.
Treballem amb dies enters, com a tot el curs fins ara:
diaPrestecés el dia del calendari intern de BiblioTech en què es va lliurar el llibre (per exemple, el dia 100 d'operació). Les dates reals ambLocalDatearriben a la lliçó 10-05.
package com.nexussoftware.bibliotech.domini;
/**
* Registre d'un prestec d'un llibre a un empleat.
* Neix amb llibre, empleat i dia de prestec, i calcula el seu propi venciment.
*/
public class Prestec {
public static final int DIES_PRESTEC = 15;
public static final double TARIFA_DIARIA = 0.25;
public static final double MULTA_MAXIMA = 20.0;
public static final int LLINDAR_LLEU = 7;
/** Dies de retard a partir dels quals la multa assoleix el sostre. */
public static final int DIES_FINS_AL_SOSTRE;
static {
DIES_FINS_AL_SOSTRE = (int) Math.ceil(MULTA_MAXIMA / TARIFA_DIARIA);
}
public static int prestecsCreats = 0;
public final Llibre llibre;
public final Empleat empleat;
public final int diaPrestec;
public final int diaVenciment; // derivat: no es demana, es calcula
public final String referencia; // identificador llegible del prestec
public int diesTranscorreguts;
public boolean retornat;
/** Constructor canonic. */
public Prestec(Llibre llibre, Empleat empleat, int diaPrestec, int diesTranscorreguts) {
if (llibre == null) {
System.out.println("AVIS: prestec sense llibre. Es registra un marcador.");
this.llibre = new Llibre("Llibre desconegut", "Desconegut", "000-0000000000", 0, false);
} else {
this.llibre = llibre;
}
if (empleat == null) {
System.out.println("AVIS: prestec sense empleat. Es registra un marcador.");
this.empleat = new Empleat("Empleat desconegut", "EMP-000");
} else {
this.empleat = empleat;
}
this.diaPrestec = Math.max(0, diaPrestec);
this.diaVenciment = this.diaPrestec + DIES_PRESTEC; // regla de negoci
if (diesTranscorreguts < 0) {
System.out.println("AVIS: dies transcorreguts negatius. Es fa servir 0.");
this.diesTranscorreguts = 0;
} else {
this.diesTranscorreguts = diesTranscorreguts;
}
this.retornat = false;
prestecsCreats++;
this.referencia = String.format("PR-%04d", prestecsCreats);
// Efectes sobre els objectes relacionats
this.llibre.prestar();
this.empleat.registrarPrestec();
}
/** Prestec acabat de fer: zero dies transcorreguts. */
public Prestec(Llibre llibre, Empleat empleat, int diaPrestec) {
this(llibre, empleat, diaPrestec, 0);
}
// ---------- Comportament (llico 03-03) ----------
public int calcularDiesRetard() {
return Math.max(0, diesTranscorreguts - DIES_PRESTEC);
}
public double calcularMulta() {
return Math.min(calcularDiesRetard() * TARIFA_DIARIA, MULTA_MAXIMA);
}
public String classificarGravetat() {
int retard = calcularDiesRetard();
if (retard == 0) return "SENSE RETARD";
if (retard <= LLINDAR_LLEU) return "LLEU";
return "GREU";
}
public boolean estaVencut() { return calcularDiesRetard() > 0; }
public int diesRestants(){ return Math.max(0, DIES_PRESTEC - diesTranscorreguts); }
public int diaDevolucioPrevista() { return diaVenciment; }
public static boolean esRetardLleu(int diesRetard) {
return diesRetard > 0 && diesRetard <= LLINDAR_LLEU;
}
}Punts que mereixen atenció:
diaVencimentés un camp derivat: no es demana a qui crida, el calcula el constructor. És impossible crear un préstec amb un venciment incoherent amb la seva data d'inici.referenciaes genera sola a partir del comptador estàtic, ambString.format(mòdul 1):PR-0001,PR-0002... Ningú no es pot inventar una referència.- El constructor té efectes sobre altres objectes: marca el llibre com a prestat i suma un préstec a l'empleat. Això és discutible en disseny avançat (un constructor "que fa coses" sorprèn), però aquí garanteix la coherència del sistema. Recorda-ho quan al mòdul 12 aparegui la separació entre construir i executar.
Ús a BiblioTechApp
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.Empleat;
import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Prestec;
public class BiblioTechApp {
public static void main(String[] args) {
System.out.println("=== BiblioTech 3.1 - Objectes que neixen complets ===\n");
// Cataleg
Llibre javaEficac = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Llibre patrons = new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994);
Llibre refactor = new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 2018);
// Plantilla
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
Empleat diego = new Empleat("Diego Alonso", "EMP-002");
Empleat nuria = new Empleat("Nuria Vidal", "EMP-003");
// Prestecs: dia 100 del calendari intern
Prestec p1 = new Prestec(javaEficac, marta, 100, 20);
Prestec p2 = new Prestec(patrons, diego, 100, 10);
Prestec p3 = new Prestec(refactor, nuria, 100, 95);
System.out.printf("%-8s %-22s %-14s %5s %5s %8s %9s %s%n",
"REF", "LLIBRE", "EMPLEAT", "INI", "VENC", "TRANSC", "MULTA", "GRAVETAT");
imprimir(p1);
imprimir(p2);
imprimir(p3);
System.out.printf("%nLlibres creats: %d | Empleats: %d | Prestecs: %d%n",
Llibre.llibresCreats, Empleat.empleatsRegistrats,
Prestec.prestecsCreats);
System.out.printf("Sostre de multa assolit als %d dies de retard.%n",
Prestec.DIES_FINS_AL_SOSTRE);
}
private static void imprimir(Prestec p) {
System.out.printf("%-8s %-22s %-14s %5d %5d %8d %8.2f %s%n",
p.referencia, p.llibre.titol, p.empleat.nom,
p.diaPrestec, p.diaVenciment, p.diesTranscorreguts,
p.calcularMulta(), p.classificarGravetat());
}
}Sortida:
=== BiblioTech 3.1 - Objectes que neixen complets === REF LLIBRE EMPLEAT INI VENC TRANSC MULTA GRAVETAT PR-0001 Java Eficac Marta Ruiz 100 115 20 1,25 LLEU PR-0002 Patrons de Disseny Diego Alonso 100 115 10 0,00 SENSE RETARD PR-0003 Refactoritzacio Nuria Vidal 100 115 95 20,00 GREU Llibres creats: 3 | Empleats: 3 | Prestecs: 3 Sostre de multa assolit als 80 dies de retard.
Compara l'arrencada de main amb la de la lliçó 03-02: sis línies de creació en lloc de trenta, i ni una sola possibilitat d'oblidar un camp.
Errors Habituals i Consells
- Posar tipus de retorn al constructor.
public void Llibre(...)és un mètode, no un constructor. És l'error número u, i el compilador no t'ajuda: la fallada apareix més tard, en fer servirnew. - Perdre el constructor per defecte sense adonar-se'n. Tan bon punt en declares un amb paràmetres,
new Llibre()deixa de compilar. Si el necessites, declara'l tu. - Posar alguna cosa abans de
this(...). No compila. Si necessites executar codi comú abans, va dins del constructor canònic. - Cridar mètodes sobreescrivibles des del constructor. És un problema subtil que s'entén del tot a 03-05: si una subclasse sobreescriu aquest mètode, s'executarà abans que els camps de la subclasse estiguin inicialitzats, veient
nulli zeros. Regla: des d'un constructor, crida només mètodesprivateofinal. - Assignar sense
thisquan hi ha shadowing.titol = titol;no fa res. L'IDE ho marca; no ignoris l'avís. - Duplicar la validació a cada constructor sobrecarregat. Encadena amb
this(...)fins al constructor canònic, i valida-hi una sola vegada. - Constructors amb efectes secundaris pesants. Llegir un fitxer o obrir una connexió en un constructor fa la classe impossible de provar. Un constructor ha d'inicialitzar, no treballar.
- Consell: fes
finaltot camp que no hagi de canviar. És gratis, ho comprova el compilador i documenta el disseny millor que un comentari. - Consell: deixa que l'IDE generi el constructor (
Alt+Insert→ Constructor a IntelliJ,Alt+Shift+S→ Generate Constructor using Fields a Eclipse) i després afegeix-hi a mà la validació.
Exercicis
Exercici 1: constructors per a una classe Reserva
Reprenent el model de sales de la lliçó 03-01, crea una classe Reserva amb camps final per a sala (String), empleat (Empleat), horaInici (int, 0-23), horaFi (int) i assistents (int), més un camp mutable activa (boolean) i un codi generat automàticament amb format RES-0001.
Requisits:
- Un constructor canònic que validi: les hores dins de 0-23,
horaFiposterior ahoraInicii assistents més gran que zero. Aplica valors per defecte segurs i avisa per consola. - Un constructor sobrecarregat que assumeixi una durada d'una hora (rep només l'hora d'inici).
- Un comptador
staticde reserves creades que alimenti el codi. - Un mètode
duracioHores().
Crea tres reserves, una d'elles amb dades invàlides, i mostra'n el resultat.
Exercici 2: traçar l'ordre d'inicialització
Escriu una classe TracaConstruccio que imprimeixi un missatge en cadascun d'aquests punts: un bloc static, un inicialitzador de camp static, un inicialitzador de camp d'instància, un bloc d'inicialització d'instància i dos constructors encadenats amb this(...). Crea dos objectes des de main i predigues la sortida abans d'executar-la. Després explica: quins missatges apareixen una sola vegada i quins dues? Per què?
Exercici 3: constructor de còpia i aliasing
Escriu un mètode static a BiblioTechApp anomenat duplicarExemplar(Llibre original) que retorni un nou Llibre amb les mateixes dades però marcat com a disponible. Demostra amb sortida per consola que:
- El duplicat i l'original són objectes diferents (
==donafalse). - Canviar la disponibilitat d'un no afecta l'altre.
- En canvi,
Llibre alies = original;sí que produeix interferència.
Després respon: si Llibre tingués un camp Empleat darrerLector (una referència mutable), n'hi hauria prou amb el constructor de còpia tal com està escrit? Quin problema apareixeria?
Solucions
Solució 1
package com.nexussoftware.bibliotech.domini;
/** Reserva d'una sala de reunions de Nexus Software. */
public class Reserva {
public static int reservesCreades = 0;
public final String sala;
public final Empleat sollicitant;
public final int horaInici;
public final int horaFi;
public final int assistents;
public final String codi;
public boolean activa;
/** Constructor canonic: unic que assigna i valida. */
public Reserva(String sala, Empleat sollicitant,
int horaInici, int horaFi, int assistents) {
this.sala = (sala == null || sala.isBlank()) ? "Sala sense nom" : sala.trim();
if (sollicitant == null) {
System.out.println("AVIS: reserva sense sollicitant. Es registra un marcador.");
this.sollicitant = new Empleat("Empleat desconegut", "EMP-000");
} else {
this.sollicitant = sollicitant;
}
// Hora d'inici dins del rang del dia
if (horaInici < 0 || horaInici > 23) {
System.out.println("AVIS: hora d'inici invalida (" + horaInici + "). Es fa servir 9.");
this.horaInici = 9;
} else {
this.horaInici = horaInici;
}
// Hora de fi: valida i posterior a l'inici
if (horaFi < 0 || horaFi > 23 || horaFi <= this.horaInici) {
int proposta = Math.min(23, this.horaInici + 1);
System.out.println("AVIS: hora de fi invalida (" + horaFi
+ "). Es fa servir " + proposta + ".");
this.horaFi = proposta;
} else {
this.horaFi = horaFi;
}
if (assistents <= 0) {
System.out.println("AVIS: nombre d'assistents invalid. Es fa servir 1.");
this.assistents = 1;
} else {
this.assistents = assistents;
}
this.activa = true;
reservesCreades++;
this.codi = String.format("RES-%04d", reservesCreades);
}
/** Reserva d'una hora: nomes s'indica l'inici. */
public Reserva(String sala, Empleat sollicitant, int horaInici, int assistents) {
this(sala, sollicitant, horaInici, horaInici + 1, assistents);
}
public int duracioHores() {
return horaFi - horaInici;
}
public void cancellar() {
if (!activa) {
System.out.println("AVIS: la reserva " + codi + " ja estava cancellada.");
return;
}
activa = false;
}
}Prova:
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
Reserva r1 = new Reserva("Sala Nord", marta, 9, 11, 6);
Reserva r2 = new Reserva("Sala Sud", marta, 16, 4); // una hora
Reserva r3 = new Reserva("Sala Est", marta, 25, 3, -2); // dades invalides
System.out.printf("%-9s %-12s %5s %5s %6s %11s%n",
"CODI", "SALA", "INI", "FI", "PERS.", "DURACIO");
System.out.printf("%-9s %-12s %5d %5d %6d %8d h%n",
r1.codi, r1.sala, r1.horaInici, r1.horaFi, r1.assistents, r1.duracioHores());
System.out.printf("%-9s %-12s %5d %5d %6d %8d h%n",
r2.codi, r2.sala, r2.horaInici, r2.horaFi, r2.assistents, r2.duracioHores());
System.out.printf("%-9s %-12s %5d %5d %6d %8d h%n",
r3.codi, r3.sala, r3.horaInici, r3.horaFi, r3.assistents, r3.duracioHores());Sortida:
AVIS: hora d'inici invalida (25). Es fa servir 9. AVIS: hora de fi invalida (3). Es fa servir 10. AVIS: nombre d'assistents invalid. Es fa servir 1. CODI SALA INI FI PERS. DURACIO RES-0001 Sala Nord 9 11 6 2 h RES-0002 Sala Sud 16 17 4 1 h RES-0003 Sala Est 9 10 1 1 h
Un detall subtil: la validació d'horaFi compara contra this.horaInici (el ja validat), no contra el paràmetre horaInici. Si comparés contra el paràmetre, una hora d'inici corregida podria deixar un interval incoherent. L'ordre de les validacions importa.
Un altre detall: el constructor d'una hora encadena amb this(...) passant horaInici + 1, així que si l'inici és invàlid, la correcció es fa una sola vegada, al canònic.
Solució 2
package com.nexussoftware.bibliotech;
public class TracaConstruccio {
static int comptadorStatic = mostrar("A. inicialitzador de camp static");
static {
mostrar("B. bloc static");
}
int campInstancia = mostrar("C. inicialitzador de camp d'instancia");
{
mostrar("D. bloc d'inicialitzacio d'instancia");
}
public TracaConstruccio() {
this(0); // ha de ser la PRIMERA sentencia
mostrar("F. cos del constructor sense arguments");
}
public TracaConstruccio(int valor) {
mostrar("E. cos del constructor canonic");
}
private static int mostrar(String missatge) {
System.out.println(missatge);
return 0;
}
public static void main(String[] args) {
System.out.println("--- primer objecte ---");
new TracaConstruccio();
System.out.println("--- segon objecte ---");
new TracaConstruccio();
}
}Sortida:
A. inicialitzador de camp static B. bloc static --- primer objecte --- C. inicialitzador de camp d'instancia D. bloc d'inicialitzacio d'instancia E. cos del constructor canonic F. cos del constructor sense arguments --- segon objecte --- C. inicialitzador de camp d'instancia D. bloc d'inicialitzacio d'instancia E. cos del constructor canonic F. cos del constructor sense arguments
Explicació. A i B apareixen una sola vegada i abans de tot: són codi de classe, i la JVM l'executa en carregar-la, cosa que passa a la primera referència a TracaConstruccio (aquí, l'execució mateixa de main). C, D, E i F apareixen dues vegades, una per objecte: són codi d'instància.
Dos matisos que sorprenen i convé interioritzar. Primer, C i D s'executen abans que qualsevol cos de constructor, en ordre textual. Segon, encara que el new sense arguments crida el constructor sense paràmetres, el missatge E apareix abans que el F: this(0) transfereix el control al canònic, que s'executa sencer, i només després continua el cos del constructor original.
Solució 3
/**
* Crea un exemplar nou amb les mateixes dades bibliografiques que l'original,
* sempre disponible per a prestec.
*/
private static Llibre duplicarExemplar(Llibre original) {
Llibre copia = new Llibre(original); // constructor de copia
copia.disponible = true; // l'exemplar nou entra al prestatge
return copia;
}Demostració:
Llibre original = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
original.prestar(); // l'original queda NO disponible
Llibre duplicat = duplicarExemplar(original);
Llibre alies = original;
System.out.println("1) original == duplicat : " + (original == duplicat)); // false
System.out.println(" original == alies : " + (original == alies)); // true
System.out.println("2) abans -> original: " + original.estaDisponible()
+ " | duplicat: " + duplicat.estaDisponible());
duplicat.prestar();
System.out.println(" despres -> original: " + original.estaDisponible()
+ " | duplicat: " + duplicat.estaDisponible());
System.out.println("3) alies.retornar() ...");
alies.retornar();
System.out.println(" original.estaDisponible(): " + original.estaDisponible());Sortida:
1) original == duplicat : false original == alies : true 2) abans -> original: false | duplicat: true despres -> original: false | duplicat: false 3) alies.retornar() ... original.estaDisponible(): true
Lectura: el duplicat és un objecte independent (passos 1 i 2), mentre que l'àlies és la mateixa caixa de memòria amb un altre nom, així que alies.retornar() canvia l'estat que veu original (pas 3). És exactament la distinció de la lliçó 03-02.
Resposta a la pregunta final. No, no n'hi hauria prou. El constructor de còpia tal com està escrit fa una còpia superficial: copia el valor de cada camp, i el valor d'un camp de tipus referència és l'adreça. Si Llibre tingués Empleat darrerLector, l'original i el duplicat apuntarien al mateix objecte Empleat, de manera que duplicat.darrerLector.registrarPrestec() alteraria també l'empleat que veu l'original. És aliasing colat per la porta del darrere.
Les sortides possibles són tres, i les veuràs desenvolupades a 03-07:
- Còpia profunda: crear també un
Empleatnou al constructor de còpia (this.darrerLector = new Empleat(altre.darrerLector);). Costós, i duplica entitats que potser han de ser úniques. - Compartir a propòsit: si l'
Empleatés una entitat compartida per tot el sistema, compartir la referència és el correcte; el que cal evitar és que algú la muti a través del llibre. - Fer immutable la classe referenciada, amb la qual cosa compartir deixa de ser perillós perquè ningú no la pot canviar.
Aquesta és la raó profunda per la qual la immutabilitat simplifica tant el disseny: quan res no pot mutar, la diferència entre copiar i compartir desapareix.
Conclusió
Has tancat la porta als objectes incomplets. Saps què és un constructor i en què es diferencia d'un mètode —començant pel detall que més maldecaps dona, que no porta tipus de retorn—; coneixes el constructor per defecte i el moment exacte en què Java deixa de regalar-te'l. Saps sobrecarregar constructors per oferir diverses formes de crear el mateix objecte i encadenar-los amb this(...) cap a un constructor canònic que valida una sola vegada, amb les seves regles estrictes. Has traçat l'ordre complet d'inicialització —blocs static en carregar la classe; després, per cada new, valors per defecte, inicialitzadors d'instància i finalment el cos del constructor— i entens per què un camp final obliga el compilador a comprovar tots els camins. Coneixes el problema del constructor telescòpic i saps que el patró Builder el resol a la lliçó 12-02. Valides al constructor, de manera provisional amb avisos i valors per defecte, sabent que el mòdul 6 portarà la solució correcta amb excepcions. I domines el constructor de còpia, l'única forma real de duplicar un objecte, amb l'advertiment que és superficial.
BiblioTech ha fet un salt: Llibre, Empleat i Prestec neixen complets, amb les seves dades identitàries marcades com a final, amb validació d'entrada, amb comptadors que no es poden oblidar i amb un Prestec que calcula el seu propi dia de venciment i la seva referència PR-0001 sense que ningú els hi dicti.
A la lliçó següent, Herència, el model creixerà cap amunt. Nexus Software no només presta llibres: també revistes tècniques i DVD de formació, cadascun amb el seu termini i la seva tarifa. En lloc de duplicar Llibre tres vegades, crearàs una classe base Material amb el que és comú i tres especialitzacions que només declarin les seves diferències. Aprendràs extends, super, l'ordre de construcció en una jerarquia i la diferència —font inesgotable de confusió— entre sobreescriure i sobrecarregar.
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
