Fins ara has estat al costat receptor: llegint stack traces i capturant el que altres llançaven. En aquesta lliçó canvies de paper. throw és la paraula amb què el teu codi declara que no pot complir el seu contracte, i throws és l'anotació amb què n'avisa a la seva signatura.

Aquest canvi de paper és el que resol, per fi, dos deutes que arrossegues des del mòdul 3. El primer: els constructors de Llibre, Empleat i Prestec validen les seves dades imprimint avisos per consola i substituint els valors invàlids per defectes —"Sense titol", any = 0—, cosa que produeix objectes que existeixen però estan malament. El segon: Cataleg.cercarPerReferencia retorna null quan no troba res, i aquell null viatja pel programa fins a explotar lluny del seu origen. Tots dos es van prometre per a aquest mòdul. Toca pagar-los.

Veuràs també la tècnica que separa el codi diagnosticable del que no ho és: l'encadenament d'excepcions. Embolcallar una fallada de baix nivell en una del vocabulari de la teva capa està bé; fer-ho perdent la causa és llençar a les escombraries l'única informació que resolia el cas. La diferència entre totes dues coses és un argument en un constructor, i veuràs exactament què desapareix del stack trace quan s'oblida.

Contingut

  1. throw: llançar explícitament
  2. Codi inabastable després d'un throw
  3. throws: declarar a la signatura
  4. Què obliga throws a qui crida
  5. Propagació en cadena
  6. Declarar excepcions no comprovades a throws
  7. Validació d'arguments: la caixa d'eines
  8. Objects.requireNonNull i les seves variants
  9. Precondicions i fallar ràpid
  10. Encadenament d'excepcions: conservar la causa
  11. Què es perd exactament si no conserves la causa
  12. Rellançar: throw e enfront d'embolcallar
  13. Traducció d'excepcions entre capes
  14. L'anàlisi precisa de rellançament de Java 7
  15. throws i sobreescriptura de mètodes
  16. BiblioTech: validacions que llancen i cerques que no retornen null
  17. Errors Comuns i Consells
  18. Exercicis

  1. throw: llançar explícitament

La sintaxi és d'una simplicitat enganyosa:

throw expressioQueDonaUnThrowable;

Tres regles:

  1. L'expressió ha d'avaluar a un objecte que descendeixi de Throwable. No pots llançar un String ni un int.
  2. L'habitual és crear l'objecte al mateix throw: throw new IllegalArgumentException("...");. Però també pots llançar una excepció que ja tinguis en una variable.
  3. Si l'expressió avalua a null, es llança un NullPointerException al seu lloc. throw null; compila i produeix un NullPointerException, no una fallada rara.

Un exemple de BiblioTech, ja amb la mentalitat correcta:

package com.nexussoftware.bibliotech.domini;

public class CalculadoraTarifa {

    private static final double TARIFA_DIARIA = 0.25;
    private static final double MULTA_MAXIMA  = 20.0;

    /**
     * Calcula la multa d'un material retornat amb retard.
     *
     * @param diesRetard dies de retard, zero o positiu
     * @return la quantitat, limitada a MULTA_MAXIMA
     * @throws IllegalArgumentException si diesRetard es negatiu
     */
    public double calcular(int diesRetard) {
        if (diesRetard < 0) {
            throw new IllegalArgumentException(
                    "Els dies de retard no poden ser negatius: " + diesRetard);
        }
        return Math.min(diesRetard * TARIFA_DIARIA, MULTA_MAXIMA);
    }

    public static void main(String[] args) {
        CalculadoraTarifa calc = new CalculadoraTarifa();
        System.out.println(calc.calcular(4));      // 1.0
        System.out.println(calc.calcular(120));    // 20.0 (limitat)
        System.out.println(calc.calcular(-3));     // <-- IllegalArgumentException
    }
}

Sortida:

1.0
20.0
Exception in thread "main" java.lang.IllegalArgumentException: Els dies de retard no poden ser negatius: -3
	at com.nexussoftware.bibliotech.domini.CalculadoraTarifa.calcular(CalculadoraTarifa.java:16)
	at com.nexussoftware.bibliotech.domini.CalculadoraTarifa.main(CalculadoraTarifa.java:26)

Compara aquest comportament amb la versió del mòdul 3, que feia això:

// VERSIO ANTIGA (modul 3): corregeix la dada i segueix
public double calcular(int diesRetard) {
    if (diesRetard < 0) {
        System.out.println("AVIS: dies negatius, s'usa 0");
        diesRetard = 0;                             // "arregla" la dada
    }
    return Math.min(diesRetard * TARIFA_DIARIA, MULTA_MAXIMA);
}

La diferència és enorme. La versió antiga inventa una dada i retorna 0.0 com si fos un càlcul legítim. Qui crida rep un número perfectament creïble que no correspon a res real, i l'avís es perd en una consola que probablement ningú no està mirant. La versió nova es nega a inventar: diu què esperava, què ha rebut i en quina línia, i atura el procés abans de propagar una dada falsa.

El principi general, que val per a tot el mòdul:

És millor una fallada sorollosa ara que un resultat incorrecte silenciós després.

  1. Codi inabastable després d'un throw

Un throw acaba el mètode immediatament, igual que un return. Qualsevol sentència posterior al mateix bloc és inabastable, i Java ho detecta en compilació:

public double calcular(int diesRetard) {
    if (diesRetard < 0) {
        throw new IllegalArgumentException("Negatiu: " + diesRetard);
        // System.out.println("mai");   // error: unreachable statement
    }
    return diesRetard * TARIFA_DIARIA;
}

Aquesta comprovació té una conseqüència pràctica molt útil: un mètode l'else del qual acaba en throw no necessita un return en aquella branca, perquè el compilador sap que aquell camí no arriba al final:

public Gravetat classificar(int diesRetard) {
    if (diesRetard < 0) {
        throw new IllegalArgumentException("Negatiu: " + diesRetard);
    }
    if (diesRetard == 0) { return Gravetat.SENSE_RETARD; }
    if (diesRetard <= LLINDAR_LLEU) { return Gravetat.LLEU; }
    return Gravetat.GREU;
    // No cal un return despres del throw inicial: aquell cami no arriba mai aqui
}

I un cas que confon: un mètode declarat amb tipus de retorn que només llança compila perfectament, sense return:

public Material cercarObligatori(String referencia) {
    throw new UnsupportedOperationException("Pendent d'implementar");
    // Compila: el metode no retorna mai normalment, aixi que no falta cap return
}

És l'idioma habitual per deixar un mètode sense implementar sense trencar la compilació. Molt millor que return null;, que compilaria igual però introduiria un null de contraban.

  1. throws: declarar a la signatura

throws va a la signatura del mètode, després de la llista de paràmetres, i anuncia quines excepcions pot propagar:

public void carregarCataleg(String cami) throws IOException {
    // ...
}

Dues paraules gairebé idèntiques amb papers oposats:

throw throws
On Dins del cos del mètode A la signatura
Què fa Llança una excepció ara Declara que es pot llançar
Quantes Una, la de l'objecte Diverses, separades per comes
Sintaxi throw new X("..."); ... metode() throws X, Y {
Obligatori Mai Sí, per a comprovades que es propaguin

Diverses excepcions se separen per comes:

public void sincronitzar(String cami) throws IOException, InterruptedException {
    // ...
}

I l'essencial: throws només és obligatori per a les excepcions comprovades. Si el teu mètode llança IllegalArgumentException (no comprovada), no has de declarar res; el codi compila igual.

// Compila sense throws: IllegalArgumentException es NO comprovada
public double calcular(int diesRetard) {
    if (diesRetard < 0) {
        throw new IllegalArgumentException("Negatiu");
    }
    return diesRetard * 0.25;
}

// NO compila sense throws: IOException es COMPROVADA
public String llegirCataleg(String cami) {
    throw new java.io.IOException("No implementat");
    // error: unreported exception IOException; must be caught or declared to be thrown
}

  1. Què obliga throws a qui crida

Quan crides un mètode que declara una excepció comprovada, el compilador et dona exactament dues opcions. És la regla de capturar o declarar.

package com.nexussoftware.bibliotech.servei;

import java.io.IOException;

public class OpcionsDeQuiCrida {

    /** Metode que declara una excepcio comprovada. */
    static String llegirCataleg(String cami) throws IOException {
        if (!cami.endsWith(".txt")) {
            throw new IOException("Format no suportat: " + cami);
        }
        return "3 materials";
    }

    // OPCIO A: capturar. Aquest metode es responsabilitza de la fallada.
    static void opcioCapturar() {
        try {
            String contingut = llegirCataleg("cataleg.csv");
            System.out.println(contingut);
        } catch (IOException e) {
            System.out.println("No s'ha pogut carregar el cataleg: " + e.getMessage());
            System.out.println("S'arrenca amb el cataleg buit.");
        }
    }

    // OPCIO B: declarar. Aquest metode delega el problema a QUI EL CRIDA.
    static void opcioDeclarar() throws IOException {
        String contingut = llegirCataleg("cataleg.csv");
        System.out.println(contingut);
    }

    // OPCIO C (illegal): ignorar. NO COMPILA.
    // static void opcioIgnorar() {
    //     llegirCataleg("cataleg.csv");
    //     // error: unreported exception IOException; must be caught or declared
    // }

    public static void main(String[] args) {
        opcioCapturar();

        // main tambe ha de triar: aqui captura
        try {
            opcioDeclarar();
        } catch (IOException e) {
            System.out.println("main: " + e.getMessage());
        }
    }
}

Com triar entre A i B, que és la decisió de disseny real:

Tria... Quan...
Capturar (A) Aquest mètode sap què fer: hi ha valor per defecte, alternativa o política de degradació
Declarar (B) Aquest mètode no sap què fer; la decisió correspon a algú amb més context

La resposta correcta sol ser B més vegades del que la gent es pensa. Un mètode de servei profund gairebé mai no té la informació necessària per decidir si una fallada de lectura ha d'avortar l'aplicació, mostrar un avís o carregar valors per defecte. Aquesta decisió pertany a una capa superior. Capturar per capturar, per "treure's l'error de sobre", és l'origen del catch buit.

Una drecera que cal conèixer: main pot declarar throws.

public static void main(String[] args) throws Exception {
    // ...
}

És perfectament legal, i significa "si això falla, que mori el programa i s'imprimeixi el trace". Per a un exemple d'aprenentatge o una eina petita és acceptable. Per a una aplicació real és una renúncia: l'usuari final veu un stack trace cru. A 06-07 el substituiràs per una frontera d'errors decent.

  1. Propagació en cadena

Quan cada mètode de la cadena tria declarar en comptes de capturar, l'excepció puja nivell a nivell fins al primer que decideixi fer-se'n càrrec. La declaració throws puja amb ella:

package com.nexussoftware.bibliotech.servei;

import java.io.IOException;

public class CadenaDePropagacio {

    // Nivell 4 (el mes profund): aqui s'origina la fallada
    static String llegirFitxer(String cami) throws IOException {
        System.out.println("  [4] llegirFitxer: intentant obrir " + cami);
        throw new IOException("No existeix el fitxer: " + cami);
    }

    // Nivell 3: no sap que fer, declara i deixa passar
    static String carregarLinies(String cami) throws IOException {
        System.out.println(" [3] carregarLinies");
        return llegirFitxer(cami);
    }

    // Nivell 2: tampoc no sap, declara i deixa passar
    static int carregarCataleg(String cami) throws IOException {
        System.out.println("[2] carregarCataleg");
        String contingut = carregarLinies(cami);
        return contingut.length();
    }

    // Nivell 1: AQUI si que hi ha context per decidir. Es captura.
    public static void main(String[] args) {
        System.out.println("[1] main: arrencant BiblioTech");
        try {
            int materials = carregarCataleg("cataleg.txt");
            System.out.println("Cataleg carregat: " + materials + " materials");
        } catch (IOException e) {
            System.out.println("[1] main: no hi ha cataleg previ (" + e.getMessage() + ")");
            System.out.println("[1] main: s'arrenca amb cataleg buit. L'aplicacio continua.");
        }
    }
}

Sortida:

[1] main: arrencant BiblioTech
[2] carregarCataleg
 [3] carregarLinies
  [4] llegirFitxer: intentant obrir cataleg.txt
[1] main: no hi ha cataleg previ (No existeix el fitxer: cataleg.txt)
[1] main: s'arrenca amb cataleg buit. L'aplicacio continua.

El recorregut, dibuixat:

flowchart TB
    M["main<br/>try-catch: DECIDEIX"]
    C2["carregarCataleg<br/>throws IOException"]
    C3["carregarLinies<br/>throws IOException"]
    C4["llegirFitxer<br/>throw new IOException"]

    M -->|"crida"| C2
    C2 -->|"crida"| C3
    C3 -->|"crida"| C4

    C4 -.->|"l'excepcio puja"| C3
    C3 -.->|"puja: nomes declara"| C2
    C2 -.->|"puja: nomes declara"| M
    M -.->|"CAPTURADA: degradacio elegant"| F["Arrenca amb cataleg buit"]

Fixa't en el repartiment de responsabilitats, que és el patró que aplicaràs a BiblioTech: els nivells 2, 3 i 4 detecten i informen; només el nivell 1 decideix. I aquesta decisió —arrencar amb catàleg buit en comptes de morir— només és possible a main, perquè només allà se sap que l'aplicació pot funcionar sense catàleg previ. llegirFitxer no tenia manera de saber-ho.

  1. Declarar excepcions no comprovades a throws

Pots posar una excepció no comprovada a throws. És legal però opcional, i el compilador la ignora completament: no obliga a res a qui crida.

// Legal. No obliga qui crida a res, pero DOCUMENTA.
public double calcular(int diesRetard) throws IllegalArgumentException {
    if (diesRetard < 0) {
        throw new IllegalArgumentException("Negatiu: " + diesRetard);
    }
    return diesRetard * TARIFA_DIARIA;
}

Val la pena? Hi ha dues escoles, i el consens és bastant clar:

Postura Argument
A favor La signatura documenta el contracte complet; alguns IDE ho aprofiten per avisar
En contra (majoritària) Embruta la signatura sense aportar comprovació; dona falsa sensació d'exhaustivitat; ningú no declara NullPointerException i tanmateix gairebé qualsevol mètode la pot llançar

La pràctica recomanada: documenta les no comprovades amb @throws al Javadoc, no a la signatura. El Javadoc és el lloc on pots explicar sota quina condició es llança, que és la informació que realment importa.

/**
 * Calcula la multa corresponent a un retard.
 *
 * @param diesRetard dies de retard; ha de ser zero o positiu
 * @return la quantitat en euros, limitada a {@value #MULTA_MAXIMA}
 * @throws IllegalArgumentException si {@code diesRetard} es negatiu
 */
public double calcular(int diesRetard) {
    // ...
}

Això sí que és útil: apareix a la documentació generada, ho mostra l'IDE en autocompletar i explica la condició, no només el tipus. La signatura queda neta.

  1. Validació d'arguments: la caixa d'eines

Arribem al deute del mòdul 3. Java té un vocabulari estàndard per rebutjar entrades invàlides, i fer servir el terme correcte importa: qui capturi podrà distingir un problema d'un altre.

Excepció Quan usar-la Exemple a BiblioTech
NullPointerException Un argument obligatori és null new Prestec(ref, null, empleat, 1)
IllegalArgumentException L'argument no és null però el seu valor és invàlid anyPublicacio = 1200; diesRetard = -3
IndexOutOfBoundsException Un índex està fora de rang Posició invàlida a la cua de reserves
IllegalStateException Els arguments són vàlids, però l'objecte no està en un estat que permeti l'operació Retornar un préstec ja retornat
UnsupportedOperationException L'operació no està suportada per aquesta implementació add sobre un catàleg de només lectura
ArithmeticException Condició aritmètica impossible La llança la JVM en la divisió entera per zero

La distinció que més costa interioritzar és IllegalArgumentException enfront d'IllegalStateException. La regla:

  • IllegalArgumentException: el problema és en el que m'has passat. Amb altres arguments, la crida funcionaria.
  • IllegalStateException: el problema és en quan m'ho has demanat. Amb els mateixos arguments, en un altre moment, funcionaria.

Aplicat a Prestec:

package com.nexussoftware.bibliotech.domini;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Prestec d'un material a un empleat.
 *
 * A partir d'aquest modul, la classe REBUTJA les dades invalides en comptes de
 * corregir-les amb avisos per consola, com feia al modul 3.
 */
public class Prestec {

    public static final int DIES_PRESTEC = 15;

    private final String referencia;          // format PR-NNNN
    private final Material material;
    private final Empleat titular;
    private final int diaInici;

    private int diaDevolucio = -1;            // -1 = encara no retornat
    private final List<Incidencia> incidencies = new ArrayList<>();

    public Prestec(String referencia, Material material, Empleat titular, int diaInici) {

        // 1. Nuls -> NullPointerException, amb Objects.requireNonNull
        this.referencia = Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
        this.material   = Objects.requireNonNull(material,   "El material no pot ser nul");
        this.titular    = Objects.requireNonNull(titular,    "El titular no pot ser nul");

        // 2. Format invalid -> IllegalArgumentException (l'argument esta malament)
        if (!referencia.matches("PR-\\d{4}")) {
            throw new IllegalArgumentException(
                    "La referencia ha de tenir el format PR-NNNN, i era: '" + referencia + "'");
        }
        if (diaInici < 1) {
            throw new IllegalArgumentException(
                    "El dia d'inici ha de ser 1 o posterior, i era: " + diaInici);
        }

        // 3. Estat incompatible -> IllegalStateException (l'argument esta be,
        //    pero l'objecte que em passes no esta en condicions)
        if (!material.estaDisponible()) {
            throw new IllegalStateException(
                    "El material " + material.getReferencia() + " no esta disponible");
        }
        if (!titular.potPrendrePrestat()) {
            throw new IllegalStateException(
                    "L'empleat " + titular.getIdentificador() + " ha assolit el limit de "
                            + Empleat.MAX_PRESTECS_SIMULTANIS + " prestecs simultanis");
        }

        this.diaInici = diaInici;
        material.prestar();
        titular.registrarPrestec();
    }

    /**
     * Registra la devolucio.
     *
     * @throws IllegalArgumentException si el dia es anterior al d'inici
     * @throws IllegalStateException    si el prestec ja estava retornat
     */
    public void registrarDevolucio(int dia) {
        // IllegalState: el problema es QUAN es demana, no QUE es demana
        if (estaRetornat()) {
            throw new IllegalStateException(
                    "El prestec " + referencia + " ja es va retornar el dia " + diaDevolucio);
        }
        // IllegalArgument: el problema es l'argument en si
        if (dia < diaInici) {
            throw new IllegalArgumentException(
                    "El dia de devolucio (" + dia + ") no pot ser anterior al d'inici ("
                            + diaInici + ")");
        }

        this.diaDevolucio = dia;
        material.retornar();
    }

    public boolean estaRetornat()      { return diaDevolucio >= 0; }
    public String  getReferencia()     { return referencia; }
    public Material getMaterial()      { return material; }
    public Empleat getTitular()        { return titular; }
    public int      getDiaInici()      { return diaInici; }

    public int calcularDiesRetard(int diaActual) {
        int diaReferencia = estaRetornat() ? diaDevolucio : diaActual;
        int exces = diaReferencia - (diaInici + DIES_PRESTEC);
        return Math.max(0, exces);
    }

    /** Incidencia anotada durant el prestec (classe imbricada estatica, 04-03). */
    public static class Incidencia {
        private final int dia;
        private final String motiu;
        private final Gravetat gravetat;

        public Incidencia(int dia, String motiu, Gravetat gravetat) {
            if (dia < 1) {
                throw new IllegalArgumentException("Dia invalid: " + dia);
            }
            this.dia = dia;
            this.motiu = Objects.requireNonNull(motiu, "El motiu no pot ser nul");
            this.gravetat = Objects.requireNonNull(gravetat, "La gravetat no pot ser nulla");
        }

        @Override
        public String toString() {
            return "Dia " + dia + " [" + gravetat + "]: " + motiu;
        }
    }
}

Compara amb la versió del mòdul 3:

// VERSIO ANTIGA: crea objectes invalids i avisa a ningu
public Prestec(String referencia, Material material, Empleat titular, int diaInici) {
    if (referencia == null || !referencia.matches("PR-\\d{4}")) {
        System.out.println("AVIS: referencia invalida, s'usa PR-0000");
        referencia = "PR-0000";                        // tots els prestecs dolents comparteixen referencia!
    }
    if (diaInici < 1) {
        System.out.println("AVIS: dia invalid, s'usa 1");
        diaInici = 1;
    }
    this.referencia = referencia;
    // ...
}

El mal de la versió antiga és pitjor del que sembla a primera vista: tots els préstecs amb referència invàlida acaben compartint PR-0000, així que l'índex Map<String, Prestec> del RegistrePrestecs els va sobreescrivint els uns als altres. Es perden préstecs en silenci. I l'avís es va imprimir fa hores en una consola que ningú no va guardar.

  1. Objects.requireNonNull i les seves variants

java.util.Objects ofereix la forma canònica de rebutjar nuls. És una línia, retorna el valor i llança NullPointerException amb el teu missatge si és null:

import java.util.Objects;

// Forma canonica: valida i assigna a la mateixa linia
this.titular = Objects.requireNonNull(titular, "El titular no pot ser nul");

És exactament equivalent a això, però en una línia i sense soroll:

if (titular == null) {
    throw new NullPointerException("El titular no pot ser nul");
}
this.titular = titular;

Les seves variants útils:

Mètode Què fa
requireNonNull(obj) Llança NullPointerException sense missatge
requireNonNull(obj, "missatge") Amb missatge. És el que has de fer servir
requireNonNull(obj, Supplier<String>) Missatge mandrós: només es construeix si falla
requireNonNullElse(obj, defecte) Retorna defecte si obj és null; no llança
requireNonNullElseGet(obj, Supplier) Igual, amb el defecte calculat mandrosament
Objects.isNull(obj) / nonNull(obj) Predicats; útils com a referències a mètode (04-06)
Objects.equals(a, b) Comparació tolerant a nuls (ja ho vas veure a 03-09)
Objects.requireNonNullElse vs checkIndex Objects.checkIndex(i, longitud) valida índexs i llança IndexOutOfBoundsException

La variant amb Supplier mereix una nota, perquè té una raó de ser concreta:

// Malament: el missatge es construeix SEMPRE, fins i tot quan no falla
Objects.requireNonNull(material, "No s'ha trobat el material " + referencia
        + " al cataleg de " + seu + " a data " + dia);

// Be: la lambda nomes s'executa si material es null
Objects.requireNonNull(material, () -> "No s'ha trobat el material " + referencia
        + " al cataleg de " + seu + " a data " + dia);

Amb la primera forma, aquella concatenació de quatre trossos passa a totes les crides, fins i tot al 99,99% que no fallen. Amb la lambda, només quan cal. És el mateix principi d'avaluació mandrosa que vas veure a 04-05 amb les lambdes, i en un mètode molt cridat la diferència és mesurable.

Un debat freqüent: NullPointerException o IllegalArgumentException per a un argument nul? Totes dues són defensables, però la convenció dominant en Java —i la que segueix el mateix JDK i Objects.requireNonNull— és NullPointerException. La raó pràctica: així, un null inesperat produeix sempre la mateixa excepció, tant si el detectes tu en validar com si explota més endavant en usar-lo. Sigues consistent: tria NullPointerException per a nuls i IllegalArgumentException per a valors no nuls però invàlids.

Un avís sobre l'ordre de validació. Aquest codi té una fallada subtil:

// MALAMENT: es fa servir referencia ABANS de comprovar que no es nulla
if (!referencia.matches("PR-\\d{4}")) {                    // NullPointerException si es null
    throw new IllegalArgumentException("Format invalid");
}
this.referencia = Objects.requireNonNull(referencia, "...");   // no hi arriba mai

Si referencia és null, salta un NullPointerException sense el teu missatge, des de matches. Valida sempre els nuls primer, i després la resta.

  1. Precondicions i fallar ràpid

Una precondició és el que un mètode exigeix per poder complir el seu contracte. calcular(int diesRetard) exigeix que diesRetard >= 0. Prestec(...) exigeix que cap argument no sigui nul, que la referència tingui format PR-NNNN i que el material estigui disponible.

Fallar ràpid (fail-fast) és el principi de comprovar aquestes precondicions al principi del mètode, abans de tocar res, i rebutjar immediatament si no es compleixen.

flowchart TB
    A["Entra al metode"] --> B["VALIDAR precondicions"]
    B --> C{"Es compleixen?"}
    C -->|"no"| D["throw: fallada immediata,<br/>amb missatge i dada culpable"]
    C -->|"si"| E["Executar la logica<br/>sense tornar a comprovar"]
    E --> F["Retornar resultat"]

    D --> G["El defecte es detecta<br/>ON i QUAN es produeix"]

Els tres avantatges, amb nom:

  1. L'error apareix a prop de la seva causa. Sense validació, un null passat al constructor explota vint minuts després en imprimir un rebut, amb un stack trace que apunta a un lloc que no en té la culpa. És exactament el cas que vas analitzar a l'exercici 2 de 06-01.
  2. La resta del mètode pot confiar. Un cop validades les precondicions, la lògica s'escriu sense defenses repetides per tot arreu.
  3. L'objecte mai no existeix en estat invàlid. Si el constructor llança, no hi ha objecte. No hi ha manera que un Prestec sense titular circuli pel programa.

Aquest tercer punt connecta directament amb els invariants de 03-07. Un invariant és una condició que es compleix durant tota la vida de l'objecte. Sense validació al constructor, l'invariant "un préstec sempre té titular" és una aspiració. Amb validació, és un fet garantit pel llenguatge.

Un idioma còmode quan hi ha moltes precondicions: mètodes privats de validació amb nom descriptiu.

public Llibre(String referencia, String titol, String autor, int anyPublicacio, String isbn) {
    validarReferencia(referencia);
    validarText(titol, "titol");
    validarText(autor, "autor");
    validarAny(anyPublicacio);
    validarIsbn(isbn);

    // A partir d'aqui, tot es valid: la logica queda neta
    this.referencia = referencia;
    this.titol = titol;
    this.autor = autor;
    this.anyPublicacio = anyPublicacio;
    this.isbn = isbn;
}

private static void validarText(String valor, String nomCamp) {
    Objects.requireNonNull(valor, "El " + nomCamp + " no pot ser nul");
    if (valor.isBlank()) {
        throw new IllegalArgumentException("El " + nomCamp + " no pot estar en blanc");
    }
}

private static void validarAny(int any) {
    if (any < 1450 || any > 2100) {
        throw new IllegalArgumentException(
                "L'any de publicacio ha d'estar entre 1450 i 2100, i era: " + any);
    }
}

private static void validarIsbn(String isbn) {
    Objects.requireNonNull(isbn, "L'ISBN no pot ser nul");
    if (!isbn.matches("97[89]-\\d{10}")) {
        throw new IllegalArgumentException(
                "ISBN amb format invalid (s'esperava 978-NNNNNNNNNN): " + isbn);
    }
}

Una advertència sobre l'abast d'aquesta tècnica: fallar ràpid és per als errors de programació i per a les dades que arriben de dins del sistema. Per a les dades que teclegi un usuari, ja vas veure a 06-02 que la política correcta és diferent: validar i reintentar amb un missatge amable, no llançar-li una excepció a la cara. La frontera entre totes dues polítiques és la capa de presentació: fora d'ella es llança, dins d'ella es pregunta.

  1. Encadenament d'excepcions: conservar la causa

Quan embolcalles una excepció en una altra, has de passar l'original com a causa. Hi ha dues formes, i una és clarament preferible.

Forma preferida: el constructor amb causa.

try {
    int any = Integer.parseInt(camps[2]);
} catch (NumberFormatException e) {
    throw new IllegalArgumentException("Linia de cataleg mal formada: " + linia, e);
    //                                                                         ^^^
    //                                                                      la causa
}

Throwable defineix quatre constructors, i totes les excepcions estàndard els ofereixen:

Constructor Ús
X() Sense missatge ni causa
X(String missatge) Amb missatge. El més freqüent en originar una fallada
X(String missatge, Throwable causa) En embolcallar. El que has de fer servir en traduir
X(Throwable causa) Missatge derivat de causa.toString(). Poc recomanable: perds l'oportunitat d'explicar

Forma alternativa: initCause. Existeix per a les excepcions antigues que no ofereixen el constructor amb causa:

IllegalArgumentException fallada = new IllegalArgumentException("Linia mal formada: " + linia);
fallada.initCause(e);               // nomes es pot cridar UNA VEGADA, i nomes si no hi havia causa
throw fallada;

initCause té dues limitacions que la fan incòmoda: només es pot cridar una vegada per objecte, i falla amb IllegalStateException si la causa ja es va establir (fins i tot si es va establir a null mitjançant el constructor de dos arguments). Fes-la servir només quan no tinguis alternativa.

Un exemple complet amb les tres capes de BiblioTech:

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Material;

/**
 * Demostra l'encadenament d'excepcions a traves de tres capes.
 */
public class CarregadorCataleg {

    /** CAPA DE DADES: converteix una linia de text en un Material. */
    private Material parsejarLinia(String linia) {
        String[] camps = linia.split(";");
        return new Llibre(camps[0].trim(), camps[1].trim(), camps[2].trim(),
                Integer.parseInt(camps[3].trim()), camps[4].trim());
        // Pot llancar: ArrayIndexOutOfBoundsException, NumberFormatException,
        //              IllegalArgumentException (validacions de Llibre)
    }

    /** CAPA DE SERVEI: tradueix al vocabulari del domini, CONSERVANT la causa. */
    public Material carregarMaterial(String linia, int numeroLinia) {
        try {
            return parsejarLinia(linia);
        } catch (RuntimeException e) {
            throw new IllegalStateException(
                    "No s'ha pogut carregar el material de la linia " + numeroLinia
                            + " del cataleg: '" + linia + "'", e);     // <-- CAUSA CONSERVADA
        }
    }

    public static void main(String[] args) {
        CarregadorCataleg carregador = new CarregadorCataleg();
        carregador.carregarMaterial("LIB-0001;Java Eficac;Bloch;mil;978-0000000001", 7);
    }
}

El stack trace resultant:

Exception in thread "main" java.lang.IllegalStateException: No s'ha pogut carregar el material de la linia 7 del cataleg: 'LIB-0001;Java Eficac;Bloch;mil;978-0000000001'
	at com.nexussoftware.bibliotech.servei.CarregadorCataleg.carregarMaterial(CarregadorCataleg.java:24)
	at com.nexussoftware.bibliotech.servei.CarregadorCataleg.main(CarregadorCataleg.java:32)
Caused by: java.lang.NumberFormatException: For input string: "mil"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:665)
	at java.base/java.lang.Integer.parseInt(Integer.java:781)
	at com.nexussoftware.bibliotech.servei.CarregadorCataleg.parsejarLinia(CarregadorCataleg.java:16)
	at com.nexussoftware.bibliotech.servei.CarregadorCataleg.carregarMaterial(CarregadorCataleg.java:22)
	... 1 more

Aquest bolcat conté les dues històries completes: quina operació de negoci ha fallat i amb quina dada (bloc 1), i quina ha estat la fallada tècnica concreta i en quina línia del codi (el Caused by:). És exactament el que necessites a les tres de la matinada.

  1. Què es perd exactament si no conserves la causa

Val la pena veure-ho amb els dos bolcats l'un al costat de l'altre, perquè la diferència se subestima fins que et toca depurar sense ella.

// VERSIO QUE PERD LA CAUSA
public Material carregarMaterialMalament(String linia, int numeroLinia) {
    try {
        return parsejarLinia(linia);
    } catch (RuntimeException e) {
        throw new IllegalStateException("No s'ha pogut carregar el material de la linia " + numeroLinia);
        //                                                                 ^ falta el ", e"
    }
}

El seu stack trace:

Exception in thread "main" java.lang.IllegalStateException: No s'ha pogut carregar el material de la linia 7
	at com.nexussoftware.bibliotech.servei.CarregadorCataleg.carregarMaterialMalament(CarregadorCataleg.java:30)
	at com.nexussoftware.bibliotech.servei.CarregadorCataleg.main(CarregadorCataleg.java:38)

I això és tot. Compara quina informació ha desaparegut:

Informació Amb causa Sense causa
Quina operació de negoci ha fallat
Quina fallada tècnica ha passat NumberFormatException Perduda
Quina dada concreta la va provocar "mil" Perduda
En quina línia del codi va passar parsejarLinia, línia 16 Perduda
Tota la pila del punt real de la fallada Completa Perduda

Amb el segon bolcat, un desenvolupador ha d'endevinar. Faltava un camp? L'any no era numèric? L'ISBN tenia format invàlid? El títol estava en blanc? Les cinc causes possibles produeixen exactament el mateix missatge. I com que el fitxer d'entrada probablement ja no existeix, ni tan sols el pot reproduir.

Aquesta informació no es recupera. Un cop l'objecte excepció original es descarta sense referenciar-lo, el recol·lector de brossa se l'endú amb tota la seva pila a dins.

Regla sense excepcions: si captures per embolcallar, passa sempre la causa. El cost són cinc caràcters.

Un cas especial en què sí que és correcte no passar la causa: quan l'excepció original conté informació sensible que no ha de sortir d'aquella capa —una contrasenya al missatge d'un error de connexió, per exemple—. En aquest cas, registra l'original amb tot el seu detall al log intern (06-07) i llança'n una de nova sense causa. És l'única excepció legítima a la regla, i és deliberada, no un oblit.

  1. Rellançar: throw e enfront d'embolcallar

Dues operacions diferents que de vegades es confonen:

// A) RELLANCAR: la mateixa excepcio, intacta
catch (RuntimeException e) {
    System.err.println("Context: processant " + referencia);
    throw e;                   // mateix objecte, mateixa pila, mateix missatge
}

// B) EMBOLCALLAR: una excepcio NOVA que duu a dins l'original
catch (RuntimeException e) {
    throw new IllegalStateException("Fallada en processar " + referencia, e);
}
Rellançar (throw e) Embolcallar
Tipus que veu qui crida L'original El nou
Stack trace Intacte: no s'afegeixen marcs Nou trace, amb Caused by:
Quan usar-ho Només volies afegir context o registrar Necessites canviar el vocabulari o el tipus
Risc Filtres el tipus d'una capa inferior Cap, si conserves la causa

Un matís tècnic sobre throw e: no reinicia el stack trace. El trace es va capturar en construir l'objecte i no es toca en rellançar, així que la línia original de la fallada es conserva. Si volguessis reiniciar-lo —cosa que gairebé mai no convé— hauries de cridar explícitament fillInStackTrace().

I una advertència sobre rellançar per costum:

// Gairebe sempre innecessari
try {
    ferAlgunaCosa();
} catch (RuntimeException e) {
    throw e;                   // no aporta RES: sense el try, el resultat seria identic
}

Un catch que només rellança sense afegir context, sense registrar i sense alliberar res és soroll pur. Esborra'l.

  1. Traducció d'excepcions entre capes

Aquest és l'ús professional de l'encadenament, i connecta directament amb l'abstracció de 03-08.

El principi: una capa no ha de filtrar les excepcions de la seva implementació interna. Si Cataleg guarda els materials en un HashMap avui i en una base de dades demà, qui l'usa no se n'hauria d'assabentar. Però si Cataleg deixa escapar una SQLException, la capa superior queda acoblada a la base de dades: tindrà catch (SQLException e) per tot arreu, i el dia que canviïs a fitxer, aquell codi deixarà de compilar.

flowchart TB
    subgraph P["Capa de PRESENTACIO"]
        P1["MenuBiblioTech<br/>Veu: excepcions del domini<br/>Mostra: missatges a l'usuari"]
    end
    subgraph S["Capa de SERVEI"]
        S1["GestorPrestecs, Cataleg<br/>Veu: excepcions de dades<br/>Tradueix a excepcions del domini"]
    end
    subgraph D["Capa de DADES"]
        D1["Fitxers, base de dades<br/>Llanca: IOException, SQLException"]
    end

    D1 -->|"IOException"| S1
    S1 -->|"MaterialNoTrobatException<br/>embolcallant la causa"| P1
    P1 -->|"'No s'ha trobat el material LIB-0001'"| U["Usuari"]

En codi:

package com.nexussoftware.bibliotech.servei;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import com.nexussoftware.bibliotech.domini.Material;

public class Cataleg {

    private final Map<String, Material> indexPerReferencia = new HashMap<>();

    /**
     * MALAMENT: filtra el detall d'implementacio.
     * El dia que canviem de fitxer a base de dades, tot el codi
     * que cridi aquest metode haura de canviar el seu catch.
     */
    public void carregarDesFitxerMalament(String cami) throws IOException {
        // ...
    }

    /**
     * BE: tradueix al vocabulari del domini, conservant la causa.
     * Qui crida nomes necessita saber que "el cataleg no s'ha pogut carregar";
     * el detall tecnic segueix disponible al Caused by: per diagnosticar.
     */
    public void carregarDesFitxer(String cami) {
        try {
            llegirFisicament(cami);                      // detall d'implementacio
        } catch (IOException e) {
            throw new IllegalStateException(
                    "No s'ha pogut carregar el cataleg des de '" + cami + "'", e);
        }
    }

    private void llegirFisicament(String cami) throws IOException {
        throw new IOException("Permis denegat: " + cami);   // simulat; el modul 7 ho fara de debo
    }
}

I la regla que governa aquesta tècnica:

Cada capa llança excepcions del vocabulari del seu propi nivell d'abstracció, i conserva com a causa les dels nivells inferiors.

És el mateix principi que apliquen els frameworks professionals. Spring converteix cada SQLException en la seva jerarquia DataAccessException —amb classes com DuplicateKeyException o DataIntegrityViolationException— precisament perquè el teu codi de negoci no depengui del motor de base de dades. A 06-04 construiràs la jerarquia equivalent per a BiblioTech.

  1. L'anàlisi precisa de rellançament de Java 7

Una nota tècnica breu però útil. Abans de Java 7, aquest codi no compilava:

public void processar() throws IOException, java.sql.SQLException {
    try {
        operacioQueLlancaTotesDues();
    } catch (Exception e) {
        registrar(e);
        throw e;                 // Java 6: error, "unreported exception Exception"
    }
}

El compilador antic raonava de manera tosca: el tipus declarat d'e és Exception, per tant throw e pot llançar qualsevol Exception, per tant el mètode ha de declarar throws Exception.

Des de Java 7, el compilador fa anàlisi precisa de rellançament (more precise rethrow): analitza quines excepcions pot llançar realment el try i dedueix que e només pot ser IOException o SQLException. El codi anterior compila sense canvis.

La condició perquè funcioni: el paràmetre del catch no s'ha de reassignar. Si el reassignes, el compilador torna a l'anàlisi conservadora. Per això convé declarar-lo final explícitament quan depens d'aquesta característica:

} catch (final Exception e) {     // el final documenta la intencio
    registrar(e);
    throw e;                      // el compilador dedueix IOException | SQLException
}

Recorda de l'apartat 5 de 06-02 que en un multi-catch el paràmetre és final implícitament, sense que hagis d'escriure-ho.

  1. throws i sobreescriptura de mètodes

Una regla d'herència que connecta directament amb el polimorfisme de 03-05 i 03-06.

Un mètode sobreescrit no pot declarar excepcions comprovades més àmplies que el mètode de la superclasse.

La raó és el principi de substitució: si tens una referència de tipus Material i crides prestar(), el compilador només sap gestionar el que Material.prestar() declara. Si una subclasse pogués llançar alguna cosa més, es colaria una excepció comprovada sense que ningú la declarés, i el sistema de comprovació es trencaria.

package com.nexussoftware.bibliotech.domini;

import java.io.IOException;
import java.io.FileNotFoundException;

class ReglesDeSobreescriptura {

    static class MaterialBase {
        /** Metode base: declara IOException. */
        public void exportar(String cami) throws IOException {
            System.out.println("Exportant " + cami);
        }
    }

    // A) Declarar la MATEIXA excepcio: LEGAL
    static class LlibreA extends MaterialBase {
        @Override
        public void exportar(String cami) throws IOException { }
    }

    // B) Declarar una SUBCLASSE: LEGAL (es mes restrictiu, mes segur)
    static class LlibreB extends MaterialBase {
        @Override
        public void exportar(String cami) throws FileNotFoundException { }
    }

    // C) NO declarar-ne cap: LEGAL (el maxim de restrictiu)
    static class LlibreC extends MaterialBase {
        @Override
        public void exportar(String cami) { }
    }

    // D) Declarar una SUPERCLASSE: ILLEGAL
    // static class LlibreD extends MaterialBase {
    //     @Override
    //     public void exportar(String cami) throws Exception { }
    //     // error: exportar(String) in LlibreD cannot override exportar(String) in MaterialBase
    //     //        overridden method does not throw Exception
    // }

    // E) Declarar una excepcio NO RELACIONADA: ILLEGAL si es comprovada
    // static class LlibreE extends MaterialBase {
    //     @Override
    //     public void exportar(String cami) throws java.sql.SQLException { }
    //     // error: overridden method does not throw SQLException
    // }

    // F) Afegir qualsevol NO COMPROVADA: LEGAL sempre (el compilador no les controla)
    static class LlibreF extends MaterialBase {
        @Override
        public void exportar(String cami) throws IllegalStateException {
            throw new IllegalStateException("El material no te contingut exportable");
        }
    }
}

Resum de la regla:

Cas Legal? Motiu
Mateixa excepció Contracte idèntic
Subclasse de la declarada Més restrictiu: mai no sorprèn qui crida
Cap El màxim de restrictiu
Superclasse de la declarada No Més ampli: qui crida no l'esperava
Comprovada no relacionada No Ídem
Qualsevol no comprovada El compilador no les controla en cap cas

Per què la substitució es trencaria amb el cas D:

MaterialBase material = new LlibreD();         // polimorfisme (03-06)
try {
    material.exportar("cataleg.txt");          // el compilador nomes veu MaterialBase.exportar
} catch (IOException e) {
    // Aqui nomes es pot capturar IOException, perque es l'unic declarat.
    // Si LlibreD pogues llancar Exception, escaparia una comprovada sense capturar
    // ni declarar: el sistema de comprovades quedaria trencat.
}

Un corol·lari pràctic: quan dissenyis una interfície o una classe abstracta (04-01, 04-02), pensa bé quin throws hi poses, perquè estàs fixant el màxim per a totes les implementacions presents i futures. És una de les raons de pes per preferir excepcions no comprovades a les interfícies de domini: no restringeixen els implementadors. És exactament el que farà Prestable a BiblioTech.

  1. BiblioTech: validacions que llancen i cerques que no retornen null

Tanquem amb els dos deutes saldats. Primer, Cataleg, que deixa de mentir amb null i false:

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Material;

/**
 * Cataleg de BiblioTech, versio del modul 6.
 *
 * Canvis respecte al modul 5:
 *  - registrar() ja no retorna un boolean mut: llanca indicant el MOTIU.
 *  - cercarPerReferencia() ja no retorna null: hi ha dos metodes amb contractes
 *    explicits, un de tolerant i un altre d'exigent.
 *
 * Nota: aqui s'usen encara excepcions estandard. A 06-04 se substitueixen per
 * la jerarquia propia de BiblioTech, que a mes transportara dades de l'error.
 */
public class Cataleg {

    private final List<Material> materials = new ArrayList<>();
    private final Map<String, Material> indexPerReferencia = new HashMap<>();
    private final Set<String> isbnRegistrats = new HashSet<>();

    /**
     * Registra un material al cataleg.
     *
     * @param material material no nul i amb referencia no registrada
     * @throws NullPointerException     si el material es nul
     * @throws IllegalArgumentException si la referencia o l'ISBN ja existeixen
     */
    public void registrar(Material material) {
        Objects.requireNonNull(material, "El material a registrar no pot ser nul");

        String referencia = material.getReferencia();
        if (indexPerReferencia.containsKey(referencia)) {
            throw new IllegalArgumentException(
                    "Ja existeix un material amb la referencia " + referencia + ": '"
                            + indexPerReferencia.get(referencia).getTitol() + "'");
        }

        if (material instanceof Llibre llibre) {
            String isbn = llibre.getIsbn();
            if (isbnRegistrats.contains(isbn)) {
                throw new IllegalArgumentException(
                        "L'ISBN " + isbn + " ja esta catalogat (referencia "
                                + referencia + ")");
            }
            isbnRegistrats.add(isbn);
        }

        materials.add(material);
        indexPerReferencia.put(referencia, material);
    }

    /**
     * Cerca un material EXIGINT que existeixi.
     *
     * Es la versio que s'usa quan l'absencia es un error: prestar,
     * retornar, consultar la fitxa d'alguna cosa que l'usuari diu tenir.
     *
     * @throws NoSuchElementException si no existeix cap material amb aquesta referencia
     */
    public Material obtenirPerReferencia(String referencia) {
        Objects.requireNonNull(referencia, "La referencia no pot ser nulla");

        Material trobat = indexPerReferencia.get(referencia);
        if (trobat == null) {
            throw new NoSuchElementException(
                    "No existeix cap material amb la referencia '" + referencia
                            + "'. El cataleg te " + materials.size() + " materials.");
        }
        return trobat;                           // MAI null: garantit pel contracte
    }

    /**
     * Cerca un material ADMETENT que no existeixi.
     *
     * Es la versio per a quan l'absencia es un resultat normal: comprovar
     * si una referencia esta lliure abans de donar-la d'alta.
     *
     * Retorna Optional en comptes de null: qui cridi no pot ignorar el cas
     * "no trobat" per descuit. Optional es desenvolupa a 10-04.
     */
    public Optional<Material> cercarPerReferencia(String referencia) {
        if (referencia == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(indexPerReferencia.get(referencia));
    }

    public boolean existeix(String referencia) {
        return referencia != null && indexPerReferencia.containsKey(referencia);
    }

    public int mida() {
        return materials.size();
    }

    public List<Material> llistar() {
        return List.copyOf(materials);           // copia immutable (03-07)
    }
}

La decisió clau està a tenir dos mètodes amb dos contractes en comptes d'un d'ambigu:

Mètode Contracte Quan usar-lo
obtenirPerReferencia Mai no retorna null; llança si no existeix L'absència és un error: prestar, retornar
cercarPerReferencia Retorna Optional, mai null L'absència és normal: comprovar disponibilitat

És un patró que veuràs per tot el JDK i per tots els frameworks: la parella get/find, require/optional, obtenir/cercar. El que desapareix per sempre és el null com a valor de retorn.

I ara l'ús conjunt, amb Empleat validant també els seus invariants:

package com.nexussoftware.bibliotech.domini;

import java.util.Objects;

/** Empleat de Nexus Software amb dret a prestec. */
public class Empleat {

    public static final int MAX_PRESTECS_SIMULTANIS = 3;

    private final String nom;
    private final String identificador;          // format EMP-NNN
    private int prestecsAcumulats;

    public Empleat(String nom, String identificador) {
        this.nom = Objects.requireNonNull(nom, "El nom no pot ser nul");
        this.identificador = Objects.requireNonNull(identificador,
                "L'identificador no pot ser nul");

        if (nom.isBlank()) {
            throw new IllegalArgumentException("El nom no pot estar en blanc");
        }
        if (!identificador.matches("EMP-\\d{3}")) {
            throw new IllegalArgumentException(
                    "L'identificador ha de tenir el format EMP-NNN, i era: '"
                            + identificador + "'");
        }
        this.prestecsAcumulats = 0;
    }

    public boolean potPrendrePrestat() {
        return prestecsAcumulats < MAX_PRESTECS_SIMULTANIS;
    }

    /**
     * Anota un prestec mes.
     *
     * @throws IllegalStateException si ja s'ha assolit el limit. Es IllegalState
     *         i no IllegalArgument perque el problema no es el que es passa
     *         (no hi ha arguments), sino l'ESTAT de l'empleat.
     */
    public void registrarPrestec() {
        if (!potPrendrePrestat()) {
            throw new IllegalStateException(
                    "L'empleat " + identificador + " (" + nom + ") ja te "
                            + prestecsAcumulats + " prestecs, el maxim es "
                            + MAX_PRESTECS_SIMULTANIS);
        }
        prestecsAcumulats++;
    }

    /**
     * Anota una devolucio.
     *
     * @throws IllegalStateException si no hi ha prestecs que retornar.
     *         Aquesta comprovacio protegeix l'INVARIANT "prestecsAcumulats >= 0",
     *         la violacio del qual va ser exactament la causa arrel del stack trace
     *         que vas analitzar a l'exercici 2 de 06-01.
     */
    public void registrarDevolucio() {
        if (prestecsAcumulats <= 0) {
            throw new IllegalStateException(
                    "L'empleat " + identificador + " no te prestecs que retornar");
        }
        prestecsAcumulats--;
    }

    public String getNom()             { return nom; }
    public String getIdentificador()   { return identificador; }
    public int getPrestecsAcumulats()  { return prestecsAcumulats; }

    @Override
    public String toString() {
        return identificador + " (" + nom + "): " + prestecsAcumulats + "/"
                + MAX_PRESTECS_SIMULTANIS + " prestecs";
    }
}

I una demostració de tot plegat:

package com.nexussoftware.bibliotech.presentacio;

import java.util.NoSuchElementException;

import com.nexussoftware.bibliotech.domini.Empleat;
import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.servei.Cataleg;

public class DemoValidacions {

    public static void main(String[] args) {
        Cataleg cataleg = new Cataleg();

        // --- Registre correcte ---
        cataleg.registrar(new Llibre("LIB-0001", "Java Eficac", "Bloch", 2018, "978-0000000001"));
        cataleg.registrar(new Llibre("LIB-0002", "Patrons de Disseny", "GoF", 1994, "978-0000000002"));
        System.out.println("Cataleg: " + cataleg.mida() + " materials");

        // --- Referencia duplicada ---
        intentar("referencia duplicada", () ->
                cataleg.registrar(new Llibre("LIB-0001", "Altre", "Altre", 2020, "978-0000000009")));

        // --- ISBN duplicat ---
        intentar("ISBN duplicat", () ->
                cataleg.registrar(new Llibre("LIB-0003", "Copia", "Bloch", 2018, "978-0000000001")));

        // --- Material nul ---
        intentar("material nul", () -> cataleg.registrar(null));

        // --- Any invalid al constructor de Llibre ---
        intentar("any invalid", () ->
                new Llibre("LIB-0004", "Manuscrit", "Anonim", 1200, "978-0000000004"));

        // --- Cerca exigent d'alguna cosa que no existeix ---
        intentar("referencia inexistent", () -> cataleg.obtenirPerReferencia("LIB-9999"));

        // --- Cerca tolerant: sense excepcio, sense null ---
        System.out.println("\nCerca tolerant de LIB-9999: "
                + cataleg.cercarPerReferencia("LIB-9999").isPresent());
        System.out.println("Cerca tolerant de LIB-0001: "
                + cataleg.cercarPerReferencia("LIB-0001")
                         .map(m -> m.getTitol())
                         .orElse("(no trobat)"));

        // --- Limit de prestecs de l'empleat ---
        Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
        for (int i = 1; i <= 3; i++) { marta.registrarPrestec(); }
        System.out.println("\n" + marta);
        intentar("quart prestec", marta::registrarPrestec);

        // --- Identificador amb format invalid ---
        intentar("identificador invalid", () -> new Empleat("Diego Alonso", "diego"));
    }

    /** Executa una accio i mostra la fallada de manera llegible. */
    private static void intentar(String descripcio, Runnable accio) {
        try {
            accio.run();
            System.out.println("[" + descripcio + "] -> no ha fallat (inesperat)");
        } catch (NullPointerException | IllegalArgumentException
                 | IllegalStateException | NoSuchElementException e) {
            System.out.println("[" + descripcio + "] -> "
                    + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }
}

Sortida:

Cataleg: 2 materials
[referencia duplicada] -> IllegalArgumentException: Ja existeix un material amb la referencia LIB-0001: 'Java Eficac'
[ISBN duplicat] -> IllegalArgumentException: L'ISBN 978-0000000001 ja esta catalogat (referencia LIB-0003)
[material nul] -> NullPointerException: El material a registrar no pot ser nul
[any invalid] -> IllegalArgumentException: L'any de publicacio ha d'estar entre 1450 i 2100, i era: 1200
[referencia inexistent] -> NoSuchElementException: No existeix cap material amb la referencia 'LIB-9999'. El cataleg te 2 materials.

Cerca tolerant de LIB-9999: false
Cerca tolerant de LIB-0001: Java Eficac

EMP-001 (Marta Ruiz): 3/3 prestecs
[quart prestec] -> IllegalStateException: L'empleat EMP-001 (Marta Ruiz) ja te 3 prestecs, el maxim es 3
[identificador invalid] -> IllegalArgumentException: L'identificador ha de tenir el format EMP-NNN, i era: 'diego'

Compara amb el que produïa el mòdul 5: set false i set null indistingibles. Ara cada fallada té nom, motiu i la dada culpable.

Queda una molèstia visible a la demostració: aquell catch amb quatre tipus estàndard units per |. És lleig, i és un símptoma. IllegalArgumentException significa coses molt diferents segons el cas —referència duplicada, ISBN duplicat, any fora de rang— i qui captura no les pot distingir sense llegir el missatge, que és text lliure i fràgil. A més, cap d'aquestes excepcions no transporta dades: per saber quina referència estava duplicada cal parsejar una cadena.

La resposta és la lliçó següent.

Errors Comuns i Consells

Confondre throw amb throws. throw llança, al cos; throws declara, a la signatura. Una lletra de diferència, papers oposats.

Corregir en silenci en comptes de rebutjar. Substituir un any invàlid per 0 o una referència invàlida per PR-0000 produeix objectes que existeixen i estan malament, i a BiblioTech provocava que els préstecs se sobreescrivissin els uns als altres a l'índex. Rebutja.

Validar el format abans de comprovar el null. referencia.matches(...) amb referencia a null llança un NullPointerException sense el teu missatge. Els nuls, primer.

Oblidar la causa en embolcallar. És l'error més car d'aquest mòdul. Perds el tipus de la fallada real, la dada culpable, la línia exacta i tota la pila. I no es recupera. El cost d'evitar-ho són cinc caràcters: , e.

Fer servir IllegalArgumentException per a tot. Distingeix: nul → NullPointerException; valor invàlid → IllegalArgumentException; moment inadequat → IllegalStateException. Qui capturi podrà reaccionar de manera diferent.

Declarar throws Exception en una interfície. Fixa el màxim per a totes les implementacions i obliga qui crida a capturar el tipus més ample possible. Declara el tipus més específic, o cap.

Posar throws de no comprovades a la signatura. Legal, però no aporta comprovació i embruta. Documenta amb @throws al Javadoc, on a més pots explicar la condició.

main(String[] args) throws Exception en una aplicació real. L'usuari final rep un stack trace cru. Acceptable en un exemple o una eina interna; en producció necessites una frontera d'errors (06-07).

Rellançar sense afegir res. Un catch que només fa throw e; és soroll: sense el try, el resultat seria idèntic. Esborra'l.

Retornar null quan alguna cosa no es troba. Endarrereix la fallada fins a un punt llunyà del programa i la fa irrecognoscible. Ofereix dos mètodes: un que llança i un altre que retorna Optional.

Consell: escriu el missatge pensant en qui el llegirà d'aquí a sis mesos a les tres de la matinada. Què ha passat, amb quina dada i què s'esperava. "Any fora de rang [1450..2100]: 1200" costa el mateix que "Any invalid".

Consell: no incloguis dades sensibles als missatges. Contrasenyes, tokens, DNI. El missatge acabarà en un log, en un correu automàtic i potser a la pantalla d'algú. A 06-07 hi ha una advertència específica sobre això.

Consell: extreu les validacions llargues a mètodes privats amb nom. validarIsbn(isbn) documenta millor que vuit línies d'if dins del constructor, i es reutilitza als setter.

Consell: Objects.requireNonNull amb el missatge sempre. La versió sense missatge llança un NullPointerException que no diu què era nul, justament la informació que necessites.

Exercicis

Exercici 1: Llibre amb validació completa

Reescriu la classe Llibre de BiblioTech eliminant completament els avisos per consola del mòdul 3 i substituint-los per excepcions. Requisits:

  • Camps: referencia (LIB-NNNN), titol, autor, anyPublicacio, isbn (978-NNNNNNNNNN o 979-...), i l'estat disponible.
  • NulsNullPointerException amb Objects.requireNonNull i missatge que identifiqui el camp.
  • Textos en blanc, formats incorrectes i any fora de [1450, 2100]IllegalArgumentException amb el valor rebut al missatge.
  • Mètodes prestar() i retornar(): prestar() sobre un material ja prestat i retornar() sobre un de disponible → IllegalStateException.
  • Extreu cada validació a un mètode privat estàtic amb nom descriptiu.
  • Afegeix Javadoc amb @throws al constructor i als dos mètodes d'estat.
  • Escriu un main que demostri cadascuna de les validacions fallant, i una construcció correcta.

Reflexiona al final en un comentari: per què prestar() sobre un material ja prestat és IllegalStateException i no IllegalArgumentException?

Exercici 2: encadenament i pèrdua d'informació

Escriu DemoEncadenament amb dues versions del mateix mètode de servei, carregarFitxaBe i carregarFitxaMalament, que parsegin una línia titol;autor;any i construeixin un record Fitxa(String titol, String autor, int any):

  • La versió bona embolcalla qualsevol RuntimeException en un IllegalStateException conservant la causa.
  • La versió dolenta fa el mateix sense la causa.

Al main:

  1. Crida totes dues amb la línia "Java Eficac;Bloch;dos mil divuit", captura l'excepció i mostra els dos stack traces complets, l'un darrere l'altre, amb un separador.
  2. Escriu un mètode analitzar(Throwable t) que recorri la cadena de causes i informi de: nombre de baules, classe de la causa arrel, missatge de la causa arrel i primera línia de codi propi de la causa arrel.
  3. Aplica'l a totes dues excepcions i mostra, en una taula impresa per consola, quina informació està disponible en cada cas.
  4. Afegeix un tercer cas amb la línia "Nomes un camp" per comprovar que també es conserva l'ArrayIndexOutOfBoundsException.

Exercici 3: GestorPrestecs amb propagació per capes

Escriu GestorPrestecs a com.nexussoftware.bibliotech.servei amb el mètode Prestec prestar(String referenciaMaterial, String idEmpleat, int dia), que orquestri Cataleg, un registre d'empleats i RegistrePrestecs.

Requisits:

  • No captura res del que no sàpiga gestionar. Les excepcions de Cataleg i Empleat pugen tal qual, tret de quan calgui traduir-les.
  • Valida els seus propis arguments amb fallada ràpida: referències no nul·les ni en blanc, dia >= 1.
  • Genera la referència del préstec amb el format PR-NNNN a partir d'un comptador.
  • Si el material existeix però no està disponible, llança IllegalStateException amb un missatge que inclogui la referència i el títol.
  • Si l'empleat ha assolit el límit, deixa que pugi l'IllegalStateException d'Empleat, però embolcalla-la en un IllegalStateException que afegeixi el context de l'operació (quin material s'intentava prestar), conservant la causa.
  • Afegeix void retornar(String referenciaPrestec, int dia) que llanci NoSuchElementException si la referència no existeix, i deixi pujar l'IllegalStateException de Prestec si ja estava retornat.

Al main, munta un escenari amb els tres empleats i els tres llibres del projecte, i provoca almenys cinc fallades diferents, mostrant per a cadascuna la classe, el missatge i —si n'hi ha— la causa.

Solucions

Solució 1

package com.nexussoftware.bibliotech.domini;

import java.util.Objects;

/**
 * Llibre del cataleg de BiblioTech.
 *
 * Versio del modul 6: REBUTJA les dades invalides en comptes de corregir-les.
 * Tota instancia que existeixi compleix els seus invariants per construccio.
 */
public class Llibre {

    private static final int ANY_MIN = 1450;     // Gutenberg
    private static final int ANY_MAX = 2100;

    private final String referencia;
    private final String titol;
    private final String autor;
    private final int anyPublicacio;
    private final String isbn;

    private boolean disponible = true;

    /**
     * Crea un llibre validant tots els seus invariants.
     *
     * @param referencia    referencia del cataleg, format LIB-NNNN
     * @param titol         titol no buit
     * @param autor         autor no buit
     * @param anyPublicacio any entre 1450 i 2100
     * @param isbn          ISBN-13, format 978-NNNNNNNNNN o 979-NNNNNNNNNN
     * @throws NullPointerException     si qualsevol argument de text es nul
     * @throws IllegalArgumentException si algun valor no compleix el seu format o rang
     */
    public Llibre(String referencia, String titol, String autor,
                  int anyPublicacio, String isbn) {

        // 1. Nuls primer: si no, els matches() de sota llancarien un NPE
        //    sense missatge util.
        Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
        Objects.requireNonNull(titol,      "El titol no pot ser nul");
        Objects.requireNonNull(autor,      "L'autor no pot ser nul");
        Objects.requireNonNull(isbn,       "L'ISBN no pot ser nul");

        // 2. Formats i rangs
        validarReferencia(referencia);
        validarTextNoBuit(titol, "titol");
        validarTextNoBuit(autor, "autor");
        validarAny(anyPublicacio);
        validarIsbn(isbn);

        // 3. A partir d'aqui tot es valid: assignacio neta
        this.referencia = referencia;
        this.titol = titol.trim();
        this.autor = autor.trim();
        this.anyPublicacio = anyPublicacio;
        this.isbn = isbn;
    }

    // ---------- Validacions ----------

    private static void validarReferencia(String referencia) {
        if (!referencia.matches("LIB-\\d{4}")) {
            throw new IllegalArgumentException(
                    "La referencia ha de tenir el format LIB-NNNN, i era: '" + referencia + "'");
        }
    }

    private static void validarTextNoBuit(String valor, String nomCamp) {
        if (valor.isBlank()) {
            throw new IllegalArgumentException(
                    "El camp '" + nomCamp + "' no pot estar buit ni contenir nomes espais");
        }
    }

    private static void validarAny(int any) {
        if (any < ANY_MIN || any > ANY_MAX) {
            throw new IllegalArgumentException(
                    "L'any de publicacio ha d'estar entre " + ANY_MIN + " i " + ANY_MAX
                            + ", i era: " + any);
        }
    }

    private static void validarIsbn(String isbn) {
        if (!isbn.matches("97[89]-\\d{10}")) {
            throw new IllegalArgumentException(
                    "L'ISBN ha de tenir el format 978-NNNNNNNNNN o 979-NNNNNNNNNN, i era: '"
                            + isbn + "'");
        }
    }

    // ---------- Estat del prestec ----------

    /**
     * Marca el llibre com a prestat.
     *
     * @throws IllegalStateException si ja estava prestat
     */
    public void prestar() {
        if (!disponible) {
            throw new IllegalStateException(
                    "El llibre " + referencia + " ('" + titol + "') ja esta prestat");
        }
        disponible = false;
    }

    /**
     * Marca el llibre com a retornat.
     *
     * @throws IllegalStateException si no estava prestat
     */
    public void retornar() {
        if (disponible) {
            throw new IllegalStateException(
                    "El llibre " + referencia + " ('" + titol + "') no esta prestat");
        }
        disponible = true;
    }

    // ---------- Accessors ----------

    public String getReferencia()   { return referencia; }
    public String getTitol()        { return titol; }
    public String getAutor()        { return autor; }
    public int getAnyPublicacio()   { return anyPublicacio; }
    public String getIsbn()         { return isbn; }
    public boolean estaDisponible() { return disponible; }

    @Override
    public String toString() {
        return referencia + " - '" + titol + "' (" + autor + ", " + anyPublicacio + ") "
                + (disponible ? "[disponible]" : "[prestat]");
    }

    // ---------- Demostracio ----------

    public static void main(String[] args) {
        System.out.println("=== Construccio correcta ===");
        Llibre valid = new Llibre("LIB-0001", "Java Eficac", "Bloch", 2018, "978-0000000001");
        System.out.println(valid);

        System.out.println("\n=== Validacions que fallen ===");
        provar("referencia nulla",
                () -> new Llibre(null, "T", "A", 2000, "978-0000000001"));
        provar("titol nul",
                () -> new Llibre("LIB-0002", null, "A", 2000, "978-0000000001"));
        provar("isbn nul",
                () -> new Llibre("LIB-0002", "T", "A", 2000, null));
        provar("referencia amb format dolent",
                () -> new Llibre("L-1", "T", "A", 2000, "978-0000000001"));
        provar("titol en blanc",
                () -> new Llibre("LIB-0002", "   ", "A", 2000, "978-0000000001"));
        provar("autor en blanc",
                () -> new Llibre("LIB-0002", "T", "", 2000, "978-0000000001"));
        provar("any massa antic",
                () -> new Llibre("LIB-0002", "T", "A", 1200, "978-0000000001"));
        provar("any al futur llunya",
                () -> new Llibre("LIB-0002", "T", "A", 3000, "978-0000000001"));
        provar("isbn amb format dolent",
                () -> new Llibre("LIB-0002", "T", "A", 2000, "12345"));

        System.out.println("\n=== Estat ===");
        valid.prestar();
        System.out.println(valid);
        provar("prestar dues vegades", valid::prestar);
        valid.retornar();
        System.out.println(valid);
        provar("retornar dues vegades", valid::retornar);
    }

    private static void provar(String descripcio, Runnable accio) {
        try {
            accio.run();
            System.out.println("  [" + descripcio + "] NO ha fallat (inesperat)");
        } catch (RuntimeException e) {
            System.out.println("  [" + descripcio + "] "
                    + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }
}

/*
 * Per que prestar() sobre un llibre ja prestat es IllegalStateException
 * i no IllegalArgumentException?
 *
 * Perque prestar() NO TE ARGUMENTS. No hi ha res que qui crida hagi
 * passat malament. El problema es l'ESTAT de l'objecte: aquest mateix llibre, en un
 * altre moment (despres de retornar-lo), acceptaria exactament la mateixa crida
 * sense cap problema.
 *
 * La regla:
 *   - IllegalArgumentException: el problema es EL QUE EM PASSES.
 *   - IllegalStateException   : el problema es QUAN M'HO DEMANES.
 *
 * Aquesta distincio no es cosmetica: permet a qui captura reaccionar de manera
 * diferent. Davant d'un IllegalArgumentException es corregeix la dada d'entrada;
 * davant d'un IllegalStateException es consulta o es canvia l'estat abans de
 * reintentar (per exemple, posant el material a la cua de reserves).
 */

Sortida (abreujada):

=== Construccio correcta ===
LIB-0001 - 'Java Eficac' (Bloch, 2018) [disponible]

=== Validacions que fallen ===
  [referencia nulla] NullPointerException: La referencia no pot ser nulla
  [titol nul] NullPointerException: El titol no pot ser nul
  [isbn nul] NullPointerException: L'ISBN no pot ser nul
  [referencia amb format dolent] IllegalArgumentException: La referencia ha de tenir el format LIB-NNNN, i era: 'L-1'
  [titol en blanc] IllegalArgumentException: El camp 'titol' no pot estar buit ni contenir nomes espais
  [any massa antic] IllegalArgumentException: L'any de publicacio ha d'estar entre 1450 i 2100, i era: 1200
  [isbn amb format dolent] IllegalArgumentException: L'ISBN ha de tenir el format 978-NNNNNNNNNN o 979-NNNNNNNNNN, i era: '12345'

=== Estat ===
LIB-0001 - 'Java Eficac' (Bloch, 2018) [prestat]
  [prestar dues vegades] IllegalStateException: El llibre LIB-0001 ('Java Eficac') ja esta prestat
LIB-0001 - 'Java Eficac' (Bloch, 2018) [disponible]
  [retornar dues vegades] IllegalStateException: El llibre LIB-0001 ('Java Eficac') no esta prestat

Solució 2

package com.nexussoftware.bibliotech.presentacio;

/**
 * Demostra, amb dos stack traces enfrontats, exactament quina informacio
 * es perd en embolcallar una excepcio sense conservar la causa.
 */
public class DemoEncadenament {

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

    // ---------- Capa de dades ----------

    /** Parseja una linia. Pot llancar diverses RuntimeException diferents. */
    private static Fitxa parsejar(String linia) {
        String[] camps = linia.split(";");
        String titol = camps[0].trim();              // ArrayIndexOutOfBounds si falta
        String autor = camps[1].trim();              // idem
        int any      = Integer.parseInt(camps[2].trim());     // NumberFormat si no es numero
        return new Fitxa(titol, autor, any);
    }

    // ---------- Capa de servei: dues versions ----------

    /** BE: embolcalla conservant la causa. */
    public static Fitxa carregarFitxaBe(String linia) {
        try {
            return parsejar(linia);
        } catch (RuntimeException e) {
            throw new IllegalStateException("No s'ha pogut carregar la fitxa de: '" + linia + "'", e);
        }
    }

    /** MALAMENT: embolcalla DESCARTANT la causa. */
    public static Fitxa carregarFitxaMalament(String linia) {
        try {
            return parsejar(linia);
        } catch (RuntimeException e) {
            throw new IllegalStateException("No s'ha pogut carregar la fitxa de: '" + linia + "'");
            //                                                                              ^ falta , e
        }
    }

    // ---------- Analisi de la cadena de causes ----------

    public record Analisi(int baules, String classeArrel, String missatgeArrel, String liniaArrel) {

        public String fila(String etiqueta) {
            return String.format("%-16s | %-9d | %-28s | %-38s | %s",
                    etiqueta, baules, classeArrel, missatgeArrel, liniaArrel);
        }
    }

    public static Analisi analitzar(Throwable t) {
        int baules = 1;
        Throwable arrel = t;

        // Recorrer fins a la causa arrel, amb proteccio anticircular
        while (arrel.getCause() != null && arrel.getCause() != arrel && baules < 20) {
            arrel = arrel.getCause();
            baules++;
        }

        String liniaArrel = "(no disponible)";
        for (StackTraceElement m : arrel.getStackTrace()) {
            if (m.getClassName().startsWith("com.nexussoftware")) {
                liniaArrel = m.getMethodName() + ":" + m.getLineNumber();
                break;
            }
        }

        String missatge = (arrel.getMessage() != null) ? arrel.getMessage() : "(sense missatge)";
        return new Analisi(baules, arrel.getClass().getSimpleName(), missatge, liniaArrel);
    }

    // ---------- Demostracio ----------

    public static void main(String[] args) {
        String liniaDolenta = "Java Eficac;Bloch;dos mil divuit";
        String liniaCurta   = "Nomes un camp";

        System.out.println("############ STACK TRACE AMB CAUSA ############");
        IllegalStateException ambCausa = capturar(() -> carregarFitxaBe(liniaDolenta));
        ambCausa.printStackTrace(System.out);

        System.out.println("\n############ STACK TRACE SENSE CAUSA ############");
        IllegalStateException senseCausa = capturar(() -> carregarFitxaMalament(liniaDolenta));
        senseCausa.printStackTrace(System.out);

        System.out.println("\n############ CAS: FALTEN CAMPS ############");
        IllegalStateException camps = capturar(() -> carregarFitxaBe(liniaCurta));
        camps.printStackTrace(System.out);

        System.out.println("\n############ COMPARATIVA ############");
        System.out.printf("%-16s | %-9s | %-28s | %-38s | %s%n",
                "VERSIO", "BAULES", "CLASSE ARREL", "MISSATGE ARREL", "LINIA ARREL");
        System.out.println("-".repeat(130));
        System.out.println(analitzar(ambCausa).fila("Amb causa"));
        System.out.println(analitzar(senseCausa).fila("Sense causa"));
        System.out.println(analitzar(camps).fila("Falten camps"));

        System.out.println("""

                CONCLUSIO
                ---------
                Amb causa: sabem que ha fallat (NumberFormatException), amb quina dada
                ("dos mil divuit") i en quina linia exacta del codi.

                Sense causa: nomes sabem que "no s'ha pogut carregar la fitxa". Les cinc
                causes possibles (falta un camp, any no numeric, titol buit,
                autor buit, any fora de rang) produeixen EXACTAMENT el mateix
                bolcat. I aquesta informacio no es pot recuperar: l'objecte
                excepcio original s'ha descartat i el recollector se l'ha endut.

                Cost d'evitar-ho: cinc caracters, ", e".""");
    }

    /** Executa una accio que s'espera que falli i retorna l'excepcio. */
    private static IllegalStateException capturar(Runnable accio) {
        try {
            accio.run();
            throw new AssertionError("S'esperava que falles");
        } catch (IllegalStateException e) {
            return e;
        }
    }
}

Sortida (abreujada):

############ STACK TRACE AMB CAUSA ############
java.lang.IllegalStateException: No s'ha pogut carregar la fitxa de: 'Java Eficac;Bloch;dos mil divuit'
	at ...DemoEncadenament.carregarFitxaBe(DemoEncadenament.java:28)
	at ...DemoEncadenament.lambda$main$0(DemoEncadenament.java:78)
	...
Caused by: java.lang.NumberFormatException: For input string: "dos mil divuit"
	at java.base/java.lang.Integer.parseInt(Integer.java:665)
	at ...DemoEncadenament.parsejar(DemoEncadenament.java:18)
	at ...DemoEncadenament.carregarFitxaBe(DemoEncadenament.java:26)
	... 4 more

############ STACK TRACE SENSE CAUSA ############
java.lang.IllegalStateException: No s'ha pogut carregar la fitxa de: 'Java Eficac;Bloch;dos mil divuit'
	at ...DemoEncadenament.carregarFitxaMalament(DemoEncadenament.java:38)
	at ...DemoEncadenament.lambda$main$1(DemoEncadenament.java:82)
	...

############ COMPARATIVA ############
VERSIO           | BAULES    | CLASSE ARREL                 | MISSATGE ARREL                         | LINIA ARREL
--------------------------------------------------------------------------------------------------------------
Amb causa        | 2         | NumberFormatException        | For input string: "dos mil divuit"      | parsejar:18
Sense causa      | 1         | IllegalStateException        | No s'ha pogut carregar la fitxa de:... | carregarFitxaMalament:38
Falten camps     | 2         | ArrayIndexOutOfBoundsException| Index 1 out of bounds for length 1     | parsejar:17

La taula resumeix el mal: a la versió sense causa, la "classe arrel" és la mateixa excepció embolcalladora i la "línia arrel" apunta a la línia del throw, que és on es va amagar el problema, no on va passar.

Solució 3

package com.nexussoftware.bibliotech.servei;

import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;

import com.nexussoftware.bibliotech.domini.Empleat;
import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Prestec;

/**
 * Orquestra prestecs i devolucions entre el cataleg, els empleats i el registre.
 *
 * Politica d'errors d'aquesta classe:
 *  - Valida els seus propis arguments amb fallada rapida.
 *  - Deixa pujar sense tocar les excepcions que ja son clares (material no trobat).
 *  - EMBOLCALLA, conservant la causa, quan pot afegir context util
 *    (quina operacio s'estava intentant).
 *  - No captura RES que no sapiga gestionar.
 */
public class GestorPrestecs {

    private final Cataleg cataleg;
    private final Map<String, Empleat> empleats = new HashMap<>();
    private final Map<String, Prestec> prestecs = new HashMap<>();

    private int comptadorPrestecs = 0;

    public GestorPrestecs(Cataleg cataleg) {
        this.cataleg = Objects.requireNonNull(cataleg, "El cataleg no pot ser nul");
    }

    public void donarAlta(Empleat empleat) {
        Objects.requireNonNull(empleat, "L'empleat no pot ser nul");
        if (empleats.containsKey(empleat.getIdentificador())) {
            throw new IllegalArgumentException(
                    "Ja existeix un empleat amb identificador " + empleat.getIdentificador());
        }
        empleats.put(empleat.getIdentificador(), empleat);
    }

    /**
     * Presta un material a un empleat.
     *
     * @throws NullPointerException     si alguna referencia es nulla
     * @throws IllegalArgumentException si alguna referencia esta en blanc o el dia es invalid
     * @throws NoSuchElementException   si el material o l'empleat no existeixen
     * @throws IllegalStateException    si el material no esta disponible o l'empleat
     *                                  ha assolit el seu limit de prestecs
     */
    public Prestec prestar(String referenciaMaterial, String idEmpleat, int dia) {

        // --- 1. Validacio d'arguments: fallada rapida ---
        Objects.requireNonNull(referenciaMaterial, "La referencia del material no pot ser nulla");
        Objects.requireNonNull(idEmpleat, "L'identificador de l'empleat no pot ser nul");

        if (referenciaMaterial.isBlank()) {
            throw new IllegalArgumentException("La referencia del material no pot estar en blanc");
        }
        if (idEmpleat.isBlank()) {
            throw new IllegalArgumentException("L'identificador de l'empleat no pot estar en blanc");
        }
        if (dia < 1) {
            throw new IllegalArgumentException("El dia ha de ser 1 o posterior, i era: " + dia);
        }

        // --- 2. Localitzar. NO es captura: obtenirPerReferencia ja llanca un
        //        NoSuchElementException amb un missatge perfecte. Embolcallar-lo
        //        no afegiria res, aixi que es deixa pujar tal qual. ---
        Llibre material = (Llibre) cataleg.obtenirPerReferencia(referenciaMaterial);

        Empleat empleat = empleats.get(idEmpleat);
        if (empleat == null) {
            throw new NoSuchElementException(
                    "No existeix cap empleat amb identificador '" + idEmpleat
                            + "'. Donats d'alta: " + empleats.keySet());
        }

        // --- 3. Regles de negoci comprovables per endavant ---
        if (!material.estaDisponible()) {
            throw new IllegalStateException(
                    "El material " + referenciaMaterial + " ('" + material.getTitol()
                            + "') no esta disponible");
        }

        // --- 4. Aqui SI que s'embolcalla: l'excepcio d'Empleat no sap que
        //        s'estava intentant prestar. Hi afegim aquest context conservant
        //        la causa, perque el Caused by: mantingui el detall. ---
        String referenciaPrestec = seguentReferencia();
        Prestec prestec;
        try {
            prestec = new Prestec(referenciaPrestec, material, empleat, dia);
        } catch (IllegalStateException e) {
            throw new IllegalStateException(
                    "No s'ha pogut prestar " + referenciaMaterial + " ('" + material.getTitol()
                            + "') a " + idEmpleat + " el dia " + dia, e);
        }

        prestecs.put(referenciaPrestec, prestec);
        return prestec;
    }

    /**
     * Registra la devolucio d'un prestec.
     *
     * @throws NoSuchElementException si la referencia del prestec no existeix
     * @throws IllegalStateException  si el prestec ja estava retornat (puja des de Prestec)
     */
    public void retornar(String referenciaPrestec, int dia) {
        Objects.requireNonNull(referenciaPrestec, "La referencia del prestec no pot ser nulla");

        Prestec prestec = prestecs.get(referenciaPrestec);
        if (prestec == null) {
            throw new NoSuchElementException(
                    "No existeix cap prestec amb referencia '" + referenciaPrestec
                            + "'. Registrats: " + prestecs.keySet());
        }

        // No es captura: si ja estava retornat, l'IllegalStateException de
        // Prestec ja diu referencia i dia. Embolcallar-lo nomes afegiria soroll.
        prestec.registrarDevolucio(dia);
        prestec.getTitular().registrarDevolucio();
    }

    private String seguentReferencia() {
        comptadorPrestecs++;
        return String.format("PR-%04d", comptadorPrestecs);
    }

    public int prestecsActius() {
        int actius = 0;
        for (Prestec p : prestecs.values()) {
            if (!p.estaRetornat()) { actius++; }
        }
        return actius;
    }

    // ------------------------------------------------------------------

    public static void main(String[] args) {
        Cataleg cataleg = new Cataleg();
        cataleg.registrar(new Llibre("LIB-0001", "Java Eficac", "Bloch", 2018, "978-0000000001"));
        cataleg.registrar(new Llibre("LIB-0002", "Patrons de Disseny", "GoF", 1994, "978-0000000002"));
        cataleg.registrar(new Llibre("LIB-0003", "Refactoritzacio", "Fowler", 1999, "978-0000000003"));

        GestorPrestecs gestor = new GestorPrestecs(cataleg);
        gestor.donarAlta(new Empleat("Marta Ruiz", "EMP-001"));
        gestor.donarAlta(new Empleat("Diego Alonso", "EMP-002"));
        gestor.donarAlta(new Empleat("Nuria Vidal", "EMP-003"));

        System.out.println("=== Prestecs correctes ===");
        System.out.println(gestor.prestar("LIB-0001", "EMP-001", 10).getReferencia());
        System.out.println(gestor.prestar("LIB-0002", "EMP-001", 10).getReferencia());
        System.out.println("Actius: " + gestor.prestecsActius());

        System.out.println("\n=== Fallades ===");
        fallar("1. Material inexistent", () -> gestor.prestar("LIB-9999", "EMP-001", 12));
        fallar("2. Empleat inexistent", () -> gestor.prestar("LIB-0003", "EMP-999", 12));
        fallar("3. Material ja prestat", () -> gestor.prestar("LIB-0001", "EMP-002", 12));
        fallar("4. Dia invalid",         () -> gestor.prestar("LIB-0003", "EMP-002", 0));
        fallar("5. Referencia nulla",    () -> gestor.prestar(null, "EMP-002", 12));

        // 6. Limit de prestecs: Marta ja en te 2, el tercer passa i el quart no
        gestor.prestar("LIB-0003", "EMP-001", 12);
        cataleg.registrar(new Llibre("LIB-0004", "Clean Code", "Martin", 2008, "978-0000000004"));
        fallar("6. Limit excedit", () -> gestor.prestar("LIB-0004", "EMP-001", 12));

        System.out.println("\n=== Devolucions ===");
        gestor.retornar("PR-0001", 30);
        System.out.println("PR-0001 retornat. Actius: " + gestor.prestecsActius());
        fallar("7. Retornar dues vegades", () -> gestor.retornar("PR-0001", 31));
        fallar("8. Prestec inexistent",    () -> gestor.retornar("PR-9999", 31));
    }

    private static void fallar(String descripcio, Runnable accio) {
        try {
            accio.run();
            System.out.println(descripcio + " -> NO ha fallat (inesperat)");
        } catch (RuntimeException e) {
            System.out.println(descripcio + " -> " + e.getClass().getSimpleName()
                    + ": " + e.getMessage());
            if (e.getCause() != null) {
                System.out.println("      Causa: " + e.getCause().getClass().getSimpleName()
                        + ": " + e.getCause().getMessage());
            }
        }
    }
}

Sortida:

=== Prestecs correctes ===
PR-0001
PR-0002
Actius: 2

=== Fallades ===
1. Material inexistent -> NoSuchElementException: No existeix cap material amb la referencia 'LIB-9999'. El cataleg te 3 materials.
2. Empleat inexistent -> NoSuchElementException: No existeix cap empleat amb identificador 'EMP-999'. Donats d'alta: [EMP-001, EMP-002, EMP-003]
3. Material ja prestat -> IllegalStateException: El material LIB-0001 ('Java Eficac') no esta disponible
4. Dia invalid -> IllegalArgumentException: El dia ha de ser 1 o posterior, i era: 0
5. Referencia nulla -> NullPointerException: La referencia del material no pot ser nulla
6. Limit excedit -> IllegalStateException: No s'ha pogut prestar LIB-0004 ('Clean Code') a EMP-001 el dia 12
      Causa: IllegalStateException: L'empleat EMP-001 (Marta Ruiz) ja te 3 prestecs, el maxim es 3

=== Devolucions ===
PR-0001 retornat. Actius: 2
7. Retornar dues vegades -> IllegalStateException: El prestec PR-0001 ja es va retornar el dia 30
8. Prestec inexistent -> NoSuchElementException: No existeix cap prestec amb referencia 'PR-9999'. Registrats: [PR-0001, PR-0002, PR-0003]

Fixa't en el cas 6, que és el que il·lustra la decisió de disseny central de l'exercici. L'excepció d'Empleat deia "EMP-001 ja té 3 préstecs", informació correcta però incompleta: no diu què s'estava intentant fer. En embolcallar-la, el missatge de dalt aporta l'operació (prestar LIB-0004 a EMP-001 el dia 12) i el Caused by: conserva el detall. Les dues peces juntes són un diagnòstic complet.

I compara amb els casos 1 i 3, on no es va embolcallar: els missatges de Cataleg i del mateix gestor ja eren complets, així que afegir una capa només hauria allargat el stack trace sense aportar res. Embolcallar per sistema és tan dolent com no embolcallar mai: es fa quan afegeixes context que la capa inferior no podia conèixer.

Un problema pendent que salta a la vista en aquest codi: el catch (IllegalStateException e) de l'apartat 4 captura qualsevol IllegalStateException que surti del constructor de Prestec, tant la del límit de préstecs com la del material no disponible. No les pot distingir sense llegir el missatge. I aquell (Llibre) del casting és igual de fràgil. Totes dues coses es resolen amb excepcions pròpies del domini, que és la lliçó següent.

Conclusió

Ja saps assenyalar una fallada, no només capturar-la. Domines throw per llançar explícitament —amb la certesa que acaba el mètode a l'instant i que el compilador rebutja el codi posterior per inabastable— i throws per declarar a la signatura, amb la regla de capturar o declarar que imposa a qui cridi: capturar si sap què fer, declarar si la decisió pertany a algú amb més context. I has vist aquella cadena de propagació funcionant en quatre nivells, on els tres profunds detecten i informen i només main decideix, perquè només main sap que BiblioTech pot arrencar amb el catàleg buit.

Saps que declarar excepcions no comprovades a throws és legal però opcional, i que la pràctica recomanada és documentar-les amb @throws al Javadoc, on pots explicar sota quina condició es llancen.

Tens el vocabulari estàndard de la validació d'arguments i la regla que l'ordena: nul → NullPointerException amb Objects.requireNonNull i el seu missatge; valor invàlid → IllegalArgumentException; moment inadequat → IllegalStateException. Amb la distinció que més costa i més rendeix: el problema és el que em passes enfront de el problema és quan m'ho demanes. Saps validar els nuls primer, fer servir la variant amb Supplier quan el missatge sigui car de construir, i extreure les validacions llargues a mètodes privats amb nom.

Entens el principi de fallar ràpid: comprovar les precondicions al principi, abans de tocar res, perquè l'error aparegui a prop de la seva causa, la resta del mètode pugui confiar i —el més important— l'objecte no arribi mai a existir en estat invàlid. És el que converteix els invariants de 03-07 d'aspiració en garantia.

I domines l'encadenament d'excepcions, que és la tècnica que separa un sistema diagnosticable d'un que no ho és. Saps que la causa es passa al constructor de dos arguments —new IllegalStateException(missatge, e)— o, en casos antics, amb initCause. Has vist exactament què desapareix quan s'oblida: el tipus de la fallada real, la dada culpable, la línia de codi i tota la pila del punt original, informació que no es recupera mai. Cinc caràcters, , e, marquen la diferència entre un diagnòstic en trenta segons i una tarda de conjectures.

Distingeixes rellançar amb throw e —mateix objecte, mateix trace, per afegir context o registrar— d'embolcallar, per canviar el vocabulari; i saps que embolcallar per sistema és tan dolent com no embolcallar mai: es fa quan aportes context que la capa inferior no podia conèixer. Coneixes la traducció entre capes que impedeix que una capa filtri les excepcions de la seva implementació interna, el mateix principi que aplica Spring en convertir SQLException en la seva jerarquia pròpia. I tens dos detalls fins: l'anàlisi precisa de rellançament de Java 7, que permet throw e des d'un catch (Exception e) sense ampliar el throws mentre no reassignis el paràmetre; i la regla de sobreescriptura, que impedeix a una subclasse declarar excepcions comprovades més àmplies que la superclasse —una raó de pes més per preferir no comprovades a les interfícies de domini.

BiblioTech ha pagat els seus dos deutes. Els constructors de Llibre, Empleat i Prestec ja no imprimeixen avisos ni inventen valors per defecte: rebutgen les dades invàlides, de manera que ja no existeixen préstecs compartint la referència PR-0000 ni llibres publicats l'any 0. Cataleg.registrar llança indicant el motiu exacte en comptes d'un false indistingible, i cercarPerReferencia ha desaparegut en la seva forma antiga: al seu lloc hi ha dos mètodes amb contractes explícits, obtenirPerReferencia —que mai no retorna null i llança si no troba— i cercarPerReferencia, que retorna Optional. El null com a valor de retorn s'ha eliminat del catàleg. I GestorPrestecs orquestra les tres peces embolcallant només on afegeix context.

Però el mateix codi de demostració ha deixat el problema següent a la vista: aquell catch amb quatre tipus estàndard units per |, i aquell catch (IllegalStateException e) que no pot distingir "el material no està disponible" de "l'empleat ha assolit el seu límit" sense llegir un missatge de text lliure. IllegalArgumentException s'està fent servir per a tres coses diferents, cap d'aquestes excepcions no transporta dades —per saber quina referència estava duplicada cal parsejar una cadena— i res no permet capturar "qualsevol fallada de BiblioTech" d'una sola vegada.

Això és la lliçó següent, Excepcions personalitzades: per què crear les teves pròpies excepcions per anomenar la fallada en el llenguatge del domini i transportar dades estructurades; com es creen estenent Exception o RuntimeException i el criteri per triar; els quatre constructors canònics i per què convé oferir-los tots; com afegir camps i accessors —getReferencia(), getLimit()— perquè qui capturi pugui decidir en comptes de llegir text; i el disseny d'una jerarquia de domini amb una arrel BiblioTechException i els seus descendents MaterialNoTrobatException, ReferenciaDuplicadaException, LimitPrestecsExceditException, MaterialNoDisponibleException i PrestecJaRetornatException, que permetrà capturar tot el domini d'una vegada. Amb les convencions de nom i de missatge, el criteri per saber quan no crear una excepció pròpia, i una nota avançada sobre fillInStackTrace per a les excepcions de control molt freqüents.

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