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

  1. Què és un constructor
  2. Constructor enfront de mètode: set diferències
  3. El constructor per defecte i quan desapareix
  4. Sobrecàrrega de constructors
  5. Encadenament amb this(...) i les seves regles
  6. Ordre d'inicialització, pas a pas
  7. Camps final i la seva inicialització obligatòria
  8. El constructor telescòpic i el patró Builder
  9. Validar al constructor
  10. El constructor de còpia
  11. Blocs static d'inicialització
  12. BiblioTech: Llibre, Empleat i Prestec amb constructors complets
  13. Errors Habituals i Consells
  14. Exercicis

  1. 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:

Llibre javaEficac = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001");

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.

  1. 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)
Pot ser static No
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.

  1. 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();       // funciona

Però 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 compilacio
error: 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:

public Llibre() {
    this("Sense titol", "Desconegut", "000-0000000000");
}

  1. 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");

  1. Encadenament amb this(...) i les seves regles

A 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:

  1. this(...) ha de ser la primera sentència del constructor. Ni un System.out.println abans.
  2. No hi pot haver alhora this(...) i super(...) (lliçó 03-05): només una de les dues, i sempre primera.
  3. No es poden formar cicles: si A() crida B() i B() crida A(), 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);
}
error: call to this must be first statement in constructor

  1. 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:

  1. Es reserva memòria i tots els camps prenen el seu valor per defecte (0, false, null).
  2. S'executen els inicialitzadors de camp i els blocs d'inicialització d'instància, en l'ordre textual en què apareixen al fitxer.
  3. 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(...).

  1. Camps final i la seva inicialització obligatòria

Ja 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.

  1. 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 —titol i isbn, tots dos String—, 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.

  1. 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.

  1. 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 diferents

Compara-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.

  1. Blocs 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:

[Prestec] Regles carregades. Sostre als 80 dies de retard.

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.

  1. BiblioTech: Llibre, Empleat i Prestec amb constructors complets

Apliquem 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 amb LocalDate arriben 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.
  • referencia es genera sola a partir del comptador estàtic, amb String.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 servir new.
  • 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 null i zeros. Regla: des d'un constructor, crida només mètodes private o final.
  • Assignar sense this quan 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 final tot 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+InsertConstructor a IntelliJ, Alt+Shift+SGenerate 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:

  1. Un constructor canònic que validi: les hores dins de 0-23, horaFi posterior a horaInici i assistents més gran que zero. Aplica valors per defecte segurs i avisa per consola.
  2. Un constructor sobrecarregat que assumeixi una durada d'una hora (rep només l'hora d'inici).
  3. Un comptador static de reserves creades que alimenti el codi.
  4. 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:

  1. El duplicat i l'original són objectes diferents (== dona false).
  2. Canviar la disponibilitat d'un no afecta l'altre.
  3. 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:

  1. Còpia profunda: crear també un Empleat nou al constructor de còpia (this.darrerLector = new Empleat(altre.darrerLector);). Costós, i duplica entitats que potser han de ser úniques.
  2. 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.
  3. 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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats