Ja tens totes les peces tècniques: saps llegir una fallada, capturar-la, llançar-la, donar-li nom propi, garantir la neteja i tancar recursos sense perdre informació. El que falta és la part que no s'aprèn llegint l'especificació del llenguatge: decidir on va cada cosa.

Perquè un sistema amb excepcions perfectament dissenyades pot continuar sent inmanejable si cada capa captura el que no li toca, si l'usuari rep un stack trace a la cara, si una fallada apareix cinc vegades al registre o —el cas de BiblioTech fins ara— si el registre no existeix i tot el diagnòstic són System.out.println que es perden en tancar el terminal.

Aquesta lliçó tanca el mòdul amb dues meitats. La primera és estratègia: on es captura, què fa cada capa, com es construeix la frontera d'errors del main, quina informació arriba a l'usuari i quina només al registre, i quan convé validar en comptes de llançar. La segona és logging: per què System.out.println no és una opció en producció, com es fa servir java.util.logging de debò, què registrar a cada nivell, què no registrar mai, i com és l'ecosistema real que veuràs a 11-07.

En acabar, BiblioTech no tindrà ni un sol System.out.println de diagnòstic, el seu main tindrà una frontera que impedeix que res escapi sense registrar-se, i els avisos seran registres amb marca de temps, nivell i context, filtrables i arxivables. És a dir: programari que es pot posar en producció.

Contingut

  1. On es captura una excepció
  2. Les capes de BiblioTech i què fa cadascuna amb els errors
  3. La frontera d'errors del main
  4. El gestor global d'excepcions no capturades
  5. Què veu l'usuari i què veu el registre
  6. Errors recuperables i irrecuperables: degradació elegant
  7. Reintents amb espera i el seu límit
  8. Validació prèvia enfront d'excepció
  9. El patró "objecte resultat" i Optional
  10. Idempotència i estat coherent
  11. Per què System.out.println no serveix en producció
  12. Els nivells de log i què registrar a cadascun
  13. java.util.logging a la pràctica
  14. Handler, Formatter i logging.properties
  15. Registrar una excepció correctament
  16. Què NO registrar mai
  17. Missatges de log útils i correlació
  18. L'ecosistema real: SLF4J, Logback i Log4j2
  19. BiblioTech: la refactorització final
  20. Errors Comuns i Consells
  21. Exercicis

  1. On es captura una excepció

La regla que resumeix tota la resta:

Captura una excepció on puguis prendre una decisió sobre ella, no on es produeix.

Sona obvi i tanmateix la major part del codi mal escrit la incompleix. El reflex d'envoltar amb try-catch qualsevol línia que pugui fallar produeix això:

// MALAMENT: captura on es produeix, sense poder decidir res
public Material cercar(String referencia) {
    try {
        return cataleg.obtenirPerReferencia(referencia);
    } catch (MaterialNoTrobatException e) {
        e.printStackTrace();       // no es una decisio
        return null;               // i ara retornem el null que tant va costar eliminar
    }
}

Aquell catch no decideix res: no recupera, no reintenta, no tradueix i no aporta context. L'única cosa que fa és degradar una excepció amb nom i dades a un null mut, desfent tota la feina de 06-03 i 06-04.

La versió correcta és no capturar:

// BE: no es captura. Puja a qui pugui decidir.
public Material cercar(String referencia) {
    return cataleg.obtenirPerReferencia(referencia);
}

Les tres preguntes que decideixen si has de capturar aquí:

  1. Puc fer alguna cosa diferent de propagar? Recuperar amb un valor per defecte, reintentar, degradar el servei. Si no, no capturis.
  2. Tinc context per afegir? Si puc embolcallar aportant informació que la capa inferior no coneixia, captura i tradueix conservant la causa (06-03).
  3. Soc la frontera? Si per damunt de mi només hi ha la JVM o l'usuari, he de capturar sí o sí, perquè ningú més no ho farà.

Si la resposta a les tres és no, deixa que pugi. No capturar és una decisió activa i moltes vegades la correcta.

  1. Les capes de BiblioTech i què fa cadascuna amb els errors

Una aplicació real s'organitza en capes, i cadascuna té una política d'errors diferent:

flowchart TB
    U["USUARI<br/>Marta, Diego, Nuria"]

    subgraph P["PRESENTACIO - MenuBiblioTech, RebutConsola"]
        P1["CAPTURA tot el que l'usuari pugui entendre<br/>Tradueix a missatges clars<br/>Registra el que es inesperat<br/>MAI no mostra un stack trace"]
    end

    subgraph S["SERVEI - GestorPrestecs, Cataleg, CuaReserves"]
        S1["Aplica regles de negoci i LLANCA excepcions del domini<br/>Tradueix les de persistencia conservant la causa<br/>Garanteix estat coherent amb finally<br/>Gairebe mai no captura"]
    end

    subgraph D["DOMINI - Material, Llibre, Empleat, Prestec"]
        D1["Valida invariants i LLANCA<br/>Fail-fast en constructors i setters<br/>MAI no captura ni registra"]
    end

    subgraph PE["PERSISTENCIA - modul 7"]
        PE1["Llanca IOException i similars<br/>El servei les tradueix"]
    end

    U -->|"introdueix dades"| P1
    P1 -->|"invoca"| S1
    S1 -->|"usa"| D1
    S1 -->|"llegeix i escriu"| PE1

    D1 -.->|"BiblioTechException"| S1
    PE1 -.->|"IOException"| S1
    S1 -.->|"BiblioTechException<br/>amb la causa conservada"| P1
    P1 -.->|"missatge clar,<br/>sense detalls interns"| U

En taula:

Capa Captura? Llança? Registra? Què mostra?
Domini Gairebé mai , sempre que es violi un invariant No Res: no coneix la interfície
Servei Només per traduir o compensar Sí, del vocabulari del domini Només el rellevant per al negoci Res
Persistència Només per traduir Sí, d'E/S Detalls tècnics Res
Presentació Sí, tot el que és esperable Poques vegades Tot el que és inesperat Missatges a l'usuari

Les dues regles que se'n deriven i que convé gravar:

El domini mai no registra ni imprimeix. Un Llibre no sap si la seva aplicació és de consola, web o mòbil, ni si hi ha algú mirant. La seva única manera de comunicar un problema és llançar. És l'abstracció de 03-08 aplicada a la gestió d'errors: cada capa parla el vocabulari del seu nivell i no pressuposa res de les de dalt.

La presentació és el filtre. És l'única capa que sap qui hi ha a l'altre costat i què pot entendre, i per tant l'única que pot decidir quin missatge mostrar. També és l'única que hauria de registrar les fallades inesperades amb tot el seu detall.

  1. La frontera d'errors del main

Per molt ben repartides que estiguin les responsabilitats, sempre pot escapar alguna cosa. La frontera d'errors és el try-catch més extern de l'aplicació, i la seva missió és que res no arribi mai a la JVM sense registrar-se i sense un missatge digne.

package com.nexussoftware.bibliotech.presentacio;

import java.util.logging.Level;
import java.util.logging.Logger;

import com.nexussoftware.bibliotech.domini.BiblioTechException;

/**
 * Punt d'entrada de BiblioTech amb frontera d'errors.
 *
 * Estructura de la frontera, del mes especific al mes general:
 *   1. BiblioTechException : fallada esperada del domini -> missatge net, sortida 1
 *   2. RuntimeException    : bug -> missatge generic amb incidencia, registre complet, sortida 2
 *   3. Error               : fallada de la JVM -> registre minim i sortida immediata, sortida 3
 */
public class BiblioTechApp {

    private static final Logger LOG = Logger.getLogger(BiblioTechApp.class.getName());

    public static void main(String[] args) {
        ConfiguracioLog.inicialitzar();
        installarGestorGlobal();

        LOG.info("BiblioTech arrencant");

        try {
            new MenuBiblioTech().arrencar();
            LOG.info("BiblioTech finalitzat correctament");

        } catch (BiblioTechException e) {
            // Fallada ESPERADA del domini que ningu no ha gestionat. Es rara, pero llegible.
            System.err.println();
            System.err.println("L'operacio no s'ha pogut completar: " + e.getMessage());
            LOG.log(Level.WARNING, "Excepcio de domini no gestionada [" + e.getCodi() + "]", e);
            System.exit(1);

        } catch (RuntimeException e) {
            // Fallada INESPERADA: es un bug. L'usuari no ha de veure els detalls.
            String incidencia = generarIdIncidencia();
            System.err.println();
            System.err.println("S'ha produit un error intern.");
            System.err.println("Incidencia: " + incidencia);
            System.err.println("Comuniqueu aquest codi a l'equip de suport de Nexus Software.");
            LOG.log(Level.SEVERE, "Error no controlat. Incidencia " + incidencia, e);
            System.exit(2);

        } catch (Error e) {
            // Fallada de la JVM: no es pot recuperar. Registrar el minim i sortir.
            // Es captura AQUI i nomes aqui, per deixar constancia abans de morir.
            System.err.println();
            System.err.println("Error irrecuperable de la maquina virtual: "
                    + e.getClass().getSimpleName());
            try {
                LOG.log(Level.SEVERE, "Error irrecuperable", e);
            } catch (Throwable ignorat) {
                // Si ni tan sols es pot registrar (p. ex. OutOfMemoryError),
                // no hi ha res mes a fer. No es propaga per no amagar l'Error.
            }
            System.exit(3);
        }
    }

    /** Identificador curt que l'usuari pot comunicar a suport. */
    private static String generarIdIncidencia() {
        return "INC-" + Long.toHexString(System.nanoTime()).toUpperCase().substring(0, 8);
    }

    private static void installarGestorGlobal() {
        Thread.setDefaultUncaughtExceptionHandler((fil, fallada) ->
                LOG.log(Level.SEVERE, "Excepcio no capturada al fil '"
                        + fil.getName() + "'", fallada));
    }
}

Les decisions que fan bona aquesta frontera:

Element Per què
Tres catch ordenats Del específic al general (06-02). BiblioTechException primer, perquè és el que produeix un missatge útil
Distingir domini de bug Una fallada de negoci mereix el seu missatge; un bug mereix una incidència i silenci sobre els detalls
Identificador d'incidència L'usuari comunica INC-A3F91C0B, i al registre es troba el stack trace exacte. Sense exposar res
Codis de sortida diferents 1 negoci, 2 bug, 3 fatal. Un script o un sistema d'integració contínua pot reaccionar de manera diferent
Capturar Error només aquí És l'única excepció a la regla de 06-01: es captura per registrar abans de morir, mai per continuar
El registre de l'Error, protegit Amb un OutOfMemoryError, fins i tot escriure al log pot fallar

Aquest és el motiu pel qual es diu que catch (Exception e) només és legítim a la frontera: aquí no hi ha ningú per damunt, així que capturar ample no amaga res de ningú.

  1. El gestor global d'excepcions no capturades

La frontera del main cobreix el fil principal. Però tan bon punt hi hagi més fils —mòdul 8—, una excepció que escapi d'un fil secundari mata aquell fil en silenci i el programa continua com si res. És una font clàssica de fallades invisibles.

Thread.setDefaultUncaughtExceptionHandler instal·la una xarxa de seguretat per a tots els fils:

package com.nexussoftware.bibliotech.presentacio;

import java.util.logging.Level;
import java.util.logging.Logger;

public final class GestorGlobal {

    private static final Logger LOG = Logger.getLogger(GestorGlobal.class.getName());

    private GestorGlobal() { }

    public static void installar() {
        Thread.setDefaultUncaughtExceptionHandler((fil, fallada) -> {
            // Aquest codi s'executa quan una excepcio escapa de QUALSEVOL fil
            LOG.log(Level.SEVERE,
                    "Excepcio no capturada al fil '" + fil.getName()
                            + "' (id " + fil.threadId() + ")", fallada);

            if (fallada instanceof Error) {
                LOG.severe("Error irrecuperable: es sollicita l'aturada de l'aplicacio");
                System.exit(3);
            }
        });

        // Hook d'aturada: ultima oportunitat de neteja (06-05)
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            LOG.info("BiblioTech aturant-se: consolidant estat");
            // Al modul 7, aqui es persistira el cataleg
        }, "aturada-bibliotech"));
    }

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

        Thread filSecundari = new Thread(() -> {
            throw new IllegalStateException("Fallada a la tasca de manteniment");
        }, "manteniment");

        filSecundari.start();

        try { filSecundari.join(); } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("El fil principal segueix viu, pero la fallada HA QUEDAT REGISTRADA");
    }
}

Sense aquell gestor, el fil manteniment moriria imprimint un stack trace a System.err que en producció probablement ningú no llegiria, i l'aplicació continuaria funcionant sense la tasca de manteniment i sense que ningú ho sabés.

Un detall de precedència: si un fil concret té el seu propi gestor (fil.setUncaughtExceptionHandler(...)), aquell té prioritat sobre el global.

  1. Què veu l'usuari i què veu el registre

Una separació que s'incompleix constantment i amb conseqüències reals:

Usuari Registre
Missatge en el seu idioma i vocabulari No necessàriament
Què fer a continuació No
Identificador d'incidència Sí (per correlacionar)
Classe de l'excepció No
Stack trace MAI Sí, complet
Camins de fitxers, noms de taules, consultes MAI
Dades internes del sistema No
Marca de temps i fil No

Un stack trace a la cara de l'usuari és un problema per tres motius, en ordre de gravetat:

  1. És un risc de seguretat. Revela paquets, classes, versions de llibreries, camins del sistema de fitxers i de vegades fragments de consultes. És informació de reconeixement gratuïta per a qui vulgui atacar el sistema.
  2. No li serveix de res. Marta Ruiz no pot fer res amb NullPointerException at Prestec.java:97.
  3. Destrueix la confiança. Un mur de text vermell comunica "això està trencat i ningú no ho controla".

La versió correcta, aplicada:

// El que veu l'usuari
System.err.println("No s'ha pogut completar el prestec.");
System.err.println("Incidencia: INC-A3F91C0B");
System.err.println("Comuniqueu aquest codi a l'equip de suport.");

// El que va al registre
LOG.log(Level.SEVERE, "Fallada en prestar LIB-0001 a EMP-001 el dia 12. Incidencia INC-A3F91C0B", e);

L'identificador d'incidència és la peça que uneix tots dos mons: l'usuari el comunica, suport el busca al registre, i apareix el stack trace complet amb tot el context. Sense exposar res.

I una regla addicional, més subtil: els missatges de les excepcions del domini sí que es poden mostrar a l'usuari, perquè estan escrits en el seu vocabulari —"El material LIB-0001 està prestat i s'espera la seva devolució el dia 27"—. Els missatges de les excepcions tècniques, no. És exactament la distinció que permet el mètode esCorregiblePerUsuari() que vas posar a BiblioTechException a 06-04.

  1. Errors recuperables i irrecuperables: degradació elegant

Recuperable Irrecuperable
Definició Existeix una alternativa acceptable No hi ha manera sensata de continuar
Resposta Degradar el servei i continuar Registrar i acabar de manera ordenada
Exemples a BiblioTech El fitxer de catàleg no existeix → arrencar buit La configuració obligatòria falta → no arrencar
La cua d'avisos no respon → apuntar i reintentar més tard OutOfMemoryError
L'exportador falla → informar i continuar operant Dades corruptes a l'índex principal

La degradació elegant consisteix a continuar donant el servei que es pugui donar, informant del que no està disponible:

package com.nexussoftware.bibliotech.servei;

import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Arrencada de BiblioTech amb degradacio elegant: l'aplicacio funciona
 * encara que faltin components opcionals, i nomes avorta si falta alguna cosa essencial.
 */
public class ArrencadaBiblioTech {

    private static final Logger LOG = Logger.getLogger(ArrencadaBiblioTech.class.getName());

    private boolean avisosDisponibles = true;
    private boolean exportacioDisponible = true;

    public Cataleg arrencar(String camiCataleg) {
        Cataleg cataleg = new Cataleg();

        // --- Component OPCIONAL 1: cataleg previ ---
        try {
            carregarCataleg(cataleg, camiCataleg);
            LOG.info(() -> "Cataleg carregat: " + cataleg.mida() + " materials");

        } catch (CatalegNoAccessibleException e) {
            // DEGRADACIO: sense cataleg previ es pot treballar; es comenca buit.
            LOG.log(Level.WARNING,
                    "No s'ha pogut carregar el cataleg des de '" + e.getCami()
                            + "'. S'arrenca amb cataleg buit.", e);
            System.out.println("Avis: no hi havia cataleg previ. Es comenca de zero.");
        }

        // --- Component OPCIONAL 2: servei d'avisos ---
        try {
            connectarServeiAvisos();
            LOG.info("Servei d'avisos disponible");
        } catch (RuntimeException e) {
            avisosDisponibles = false;
            LOG.log(Level.WARNING, "Servei d'avisos no disponible; s'acumularan en cua", e);
            System.out.println("Avis: els avisos de venciment no s'enviaran en aquesta sessio.");
        }

        // --- Component ESSENCIAL: configuracio ---
        // Si aixo falla, NO es degrada: s'avorta. Continuar amb una tarifa
        // desconeguda produiria multes incorrectes, que es pitjor que no arrencar.
        int diesPrestec = llegirConfiguracioObligatoria("dies.prestec");
        LOG.config(() -> "Configuracio: dies.prestec=" + diesPrestec);

        return cataleg;
    }

    public boolean potEnviarAvisos()  { return avisosDisponibles; }
    public boolean potExportar()      { return exportacioDisponible; }

    private void carregarCataleg(Cataleg cataleg, String cami) throws CatalegNoAccessibleException {
        throw new CatalegNoAccessibleException(cami,
                new java.io.FileNotFoundException(cami + " (No such file or directory)"));
    }

    private void connectarServeiAvisos() {
        throw new IllegalStateException("El servei d'avisos no respon al port 8080");
    }

    private int llegirConfiguracioObligatoria(String clau) {
        return 15;   // al modul 7 vindra d'un fitxer .properties
    }
}

El criteri per decidir entre degradar i avortar:

Degrada quan el servei reduït continuï sent correcte. Avorta quan continuar produiria resultats incorrectes.

Arrencar sense catàleg previ dona un servei reduït però correcte: la biblioteca està buida i això és cert. Arrencar sense conèixer la tarifa diària produiria multes incorrectes, que és molt pitjor que no arrencar: un sistema que dona números equivocats amb tota naturalitat és més perillós que un que no arrenca.

  1. Reintents amb espera i el seu límit

Ja vas veure el patró a 06-02. Aquí van les regles de decisió, que és el que s'aprèn amb l'experiència:

Què reintentar i què no:

Reintentar No reintentar
Fallada de xarxa o de connexió IllegalArgumentException: la dada continuarà sent invàlida
Fitxer temporalment bloquejat MaterialNoTrobatException: continuarà sense existir
Servei que retorna "no disponible" LimitPrestecsExceditException: la regla no canvia sola
Bloqueig de base de dades Qualsevol error de programació

La regla: reintenta el que depèn de l'entorn; mai el que depèn de les dades o de les regles.

Les quatre condicions d'un reintent correcte:

  1. Límit d'intents. Sense ell, un servei caigut converteix la teva aplicació en un bucle infinit.
  2. Espera creixent (backoff exponencial): 200, 400, 800, 1600 ms. Reintentar en ràfega agreuja la caiguda del servei remot.
  3. Fallada definitiva en esgotar-los, conservant l'últim error com a causa. Mai retornar null fingint èxit.
  4. Idempotència de l'operació. Aquesta és la que més s'oblida: si el préstec es va registrar i només va fallar la confirmació, reintentar crearà dos préstecs. Només es reintenta el que es pot repetir sense efectes secundaris.

Un refinament que veuràs en sistemes reals: el tallacircuits (circuit breaker). Si un servei falla repetidament, es deixa de cridar durant un temps en comptes de reintentar a cada operació. És un patró d'arquitectura que queda fora de l'abast d'aquest curs, però convé conèixer-ne el nom.

  1. Validació prèvia enfront d'excepció

Moltes vegades pots triar entre preguntar abans o intentar i capturar. La taula de criteris:

Criteri Validació prèvia (if) Excepció
Freqüència de la fallada Alta: és un cas normal Baixa: és excepcional
Cost de comprovar Barat (isEmpty, containsKey) Car o impossible sense intentar-ho
Condició de cursa Pot canviar entre la comprovació i l'ús Immune: és atòmic
Llegibilitat Millor si són una o dues condicions Millor si són moltes o estan disperses
Rendiment ~70 vegades més ràpid (06-02) Irrellevant si és rar

Exemples aplicats:

// VALIDACIO PREVIA: barata, el cas es frequent i no hi ha cursa
if (cataleg.existeix(referencia)) {
    Material m = cataleg.obtenirPerReferencia(referencia);
    // ...
}

// Millor encara amb el metode que retorna Optional
cataleg.cercarPerReferencia(referencia)
       .ifPresent(m -> System.out.println(m.getTitol()));

// EXCEPCIO: la conversio no es pot validar sense fer-la
try {
    int dies = Integer.parseInt(entrada);
} catch (NumberFormatException e) {
    // Escriure un validador d'enters correcte (signe, rang, desbordament)
    // es mes propens a errors que capturar
}

// EXCEPCIO OBLIGATORIA: condicio de cursa
// Amb fitxers o recursos compartits, "comprovar i despres usar" no es atomic:
// entre l'exists() i l'open() un altre proces pot haver esborrat el fitxer.
try (BufferedReader r = new BufferedReader(new FileReader(cami))) {
    // ...
} catch (FileNotFoundException e) {
    // L'unica forma fiable
}

Aquell últim cas té nom propi, TOCTOU (time-of-check to time-of-use), i és una font coneguda de fallades i de vulnerabilitats. Sempre que el recurs sigui compartit —fitxers, base de dades, memòria entre fils—, la comprovació prèvia no garanteix res i cal gestionar l'excepció de totes maneres.

La regla pràctica final: valida el que sigui barat i estable de comprovar; captura el que no es pugui saber sense intentar-ho.

  1. El patró "objecte resultat" i Optional

No tota fallada ha de ser una excepció. Hi ha dues alternatives legítimes.

Optional, per a l'absència de valor:

// Ja el fas servir a BiblioTech des de 06-03
public Optional<Material> cercarPerReferencia(String referencia) {
    return Optional.ofNullable(indexPerReferencia.get(referencia));
}

Comunica "potser no hi ha res, i això és normal" d'una manera que el compilador ajuda a no ignorar. Es desenvolupa a 10-04.

L'objecte resultat, quan necessites transportar el motiu de la fallada sense llançar. És especialment útil en validacions en lot, on vols recollir tots els errors i no només el primer:

package com.nexussoftware.bibliotech.servei;

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

/**
 * Resultat d'una operacio que pot fallar, SENSE llancar.
 *
 * Util quan vols acumular DIVERSOS errors en comptes d'avortar al primer:
 * validar un formulari, importar un fitxer, comprovar regles de negoci.
 */
public class Resultat {

    private final boolean exit;
    private final List<String> errors;

    private Resultat(boolean exit, List<String> errors) {
        this.exit = exit;
        this.errors = List.copyOf(errors);
    }

    public static Resultat ok() {
        return new Resultat(true, List.of());
    }

    public static Resultat ambErrors(List<String> errors) {
        return new Resultat(false, errors);
    }

    public boolean esExit()        { return exit; }
    public List<String> getErrors() { return errors; }

    public String descriure() {
        if (exit) { return "OK"; }
        return "Rebutjat (" + errors.size() + " problemes):\n  - "
                + String.join("\n  - ", errors);
    }

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

    /** Exemple: validar una alta acumulant TOTS els problemes. */
    public static Resultat validarAlta(String referencia, String titol, int any, String isbn) {
        List<String> errors = new ArrayList<>();

        if (referencia == null || !referencia.matches("LIB-\\d{4}")) {
            errors.add("La referencia ha de tenir el format LIB-NNNN, i era: " + referencia);
        }
        if (titol == null || titol.isBlank()) {
            errors.add("El titol no pot estar buit");
        }
        if (any < 1450 || any > 2100) {
            errors.add("L'any ha d'estar entre 1450 i 2100, i era: " + any);
        }
        if (isbn == null || !isbn.matches("97[89]-\\d{10}")) {
            errors.add("L'ISBN ha de tenir el format 978-NNNNNNNNNN, i era: " + isbn);
        }

        return errors.isEmpty() ? ok() : ambErrors(errors);
    }

    public static void main(String[] args) {
        System.out.println(validarAlta("LIB-0001", "Java Eficac", 2018, "978-0000000001")
                .descriure());
        System.out.println();
        System.out.println(validarAlta("L-1", "", 1200, "12345").descriure());
    }
}

Sortida:

OK

Rebutjat (4 problemes):
  - La referencia ha de tenir el format LIB-NNNN, i era: L-1
  - El titol no pot estar buit
  - L'any ha d'estar entre 1450 i 2100, i era: 1200
  - L'ISBN ha de tenir el format 978-NNNNNNNNNN, i era: 12345

Amb excepcions, l'usuari hauria corregit la referència, reenviat, descobert el del títol, corregit, reenviat... quatre voltes. Amb l'objecte resultat, veu els quatre problemes de cop.

Quan usar cada mecanisme:

Mecanisme Quan
Excepció La fallada és excepcional i qui crida normalment no pot continuar
Optional L'absència de valor és un resultat normal
Objecte resultat Hi ha diversos errors a comunicar alhora, o la fallada és tan freqüent que forma part del flux
boolean Només quan "no" és una resposta legítima i única (Set.add)

  1. Idempotència i estat coherent

Dues propietats que fan que un sistema sobrevisqui a les fallades.

Idempotent significa que repetir l'operació produeix el mateix resultat que fer-la una vegada.

// NO idempotent: dues crides, dos prestecs
gestor.prestar("LIB-0001", "EMP-001", 12);

// Idempotent: la segona crida no fa res
sessio.close();
cuaReserves.cancellar("LIB-0001", "EMP-001");   // si no n'hi havia, no passa res

Per què importa aquí: una operació idempotent es pot reintentar sense por. Si no ho és, un reintent després d'una fallada de xarxa pot duplicar l'efecte. És la quarta condició de l'apartat 7.

La tècnica habitual per fer idempotent una cosa que no ho és de manera natural: un identificador d'operació que permeti detectar el duplicat.

public Prestec prestar(String idOperacio, String referencia, String idEmpleat, int dia) {
    // Si aquesta operacio ja s'ha processat, es retorna el resultat anterior
    Prestec jaFet = operacionsProcessades.get(idOperacio);
    if (jaFet != null) {
        LOG.info(() -> "Operacio " + idOperacio + " ja processada; es retorna el resultat previ");
        return jaFet;
    }
    // ... proces normal ...
}

Estat coherent és el que vas resoldre a 06-05 amb el patró de marcadors i compensació al finally. La regla que ho resumeix:

Una operació ha de deixar el sistema en un estat vàlid, tant si acaba com si falla. Mai a mitges.

I l'ordre que ho facilita, aplicant el de 06-03:

  1. Validar-ho tot (no modifica res).
  2. Preparar els objectes nous (encara no visibles per a ningú).
  3. Aplicar els canvis, com més tard i com més junts millor.
  4. Compensar al finally si no s'ha arribat al final.

  1. Per què System.out.println no serveix en producció

Comença la segona meitat. Tot el diagnòstic de BiblioTech fins ara ha estat System.out.println, i cal substituir-lo. Aquests són els set motius:

Problema Conseqüència real
No es pot filtrar O ho veus tot o no veus res. Amb mil línies per minut, trobar la rellevant és impossible
No es pot desactivar El diagnòstic de depuració continua imprimint-se en producció, amb el seu cost
No té marca de temps "Quan va passar això? Abans o després d'allò?" Sense resposta
No té nivell Un avís rutinari i una fallada greu es veuen exactament igual
No diu d'on ve Ni classe ni mètode ni fil. En un programa multifil, és il·legible
No s'arxiva ni rota Es perd en tancar el terminal. I si redirigeixes a fitxer, creix sense límit fins a omplir el disc
És síncron i bloquejant Escriure a consola bloqueja el fil. En un bucle calent, enfonsa el rendiment

Compara la mateixa informació:

// System.out.println
System.out.println("AVIS: referencia duplicada");
AVIS: referencia duplicada
// Logger
LOG.warning("Alta rebutjada: referencia LIB-0001 duplicada, ja la fa servir 'Java Eficac'");
2026-08-05 11:42:07.312 [main] WARNING com.nexussoftware.bibliotech.servei.Cataleg registrar
  Alta rebutjada: referencia LIB-0001 duplicada, ja la fa servir 'Java Eficac'

El segon es pot filtrar per nivell, per classe o per fil; es pot arxivar i rotar; es pot desactivar sense tocar el codi; i diu quan, on i què.

I una regla que confon molta gent:

System.out.println continua sent correcte per a la SORTIDA del programa, és a dir, per al que l'usuari ha demanat veure: el menú, el llistat del catàleg, el rebut. El que cal substituir és el diagnòstic: els avisos, les traces, els errors.

La prova per distingir-los: això ho ha demanat l'usuari? Si sí, és sortida. Si no, és log.

  1. Els nivells de log i què registrar a cadascun

java.util.logging defineix set nivells. Aquests són, amb la seva equivalència a SLF4J/Logback, que és el que veuràs a l'ecosistema real:

java.util.logging SLF4J/Logback Quan usar-lo Exemple a BiblioTech
SEVERE ERROR El sistema no pot fer la seva feina. Requereix intervenció Índex del catàleg corrupte; error no controlat
WARNING WARN Alguna cosa va malament però s'ha pogut continuar No hi havia catàleg previ; el servei d'avisos no respon
INFO INFO Fites del funcionament normal Arrencada, aturada, préstec completat, importació acabada
CONFIG DEBUG Paràmetres de configuració en arrencar dies.prestec=15, tarifa.diaria=0.25
FINE DEBUG Detall útil per depurar Entrada i sortida dels mètodes de servei
FINER TRACE Més detall encara Cada iteració d'un bucle d'importació
FINEST TRACE Màxim detall Bolcat d'estructures internes

La guia pràctica, que és el que de debò cal interioritzar:

SEVERE/ERROR — Algú ha de mirar això. Si registres a aquest nivell alguna cosa que no requereix intervenció, estàs entrenant l'equip a ignorar els errors. És la fallada més comuna dels sistemes de log mal configurats: quan tot és un error, res no ho és.

WARNING/WARN — Ha anat malament però continuem. Degradacions, reintents, configuracions sospitoses, ús de valors per defecte perquè faltava un paràmetre.

INFO — Les fites del negoci. Ha de poder llegir-se com la història del que ha fet l'aplicació. Si té tant soroll que no es pot llegir, hi ha coses que haurien de ser FINE.

CONFIG/FINE/FINER/FINEST — Desactivats en producció per defecte. S'activen quan cal investigar alguna cosa concreta. Per això han de ser barats quan estan desactivats, que és de què tracta l'apartat següent.

I un error de criteri molt freqüent: una excepció capturada i gestionada correctament no és un SEVERE. Si el catàleg no existia i vas arrencar buit, això és un WARNING: el sistema va fer el previst. SEVERE és per al que ningú no ha previst.

  1. java.util.logging a la pràctica

java.util.logging (JUL) ve al JDK, sense dependències. És l'elecció per a aquest curs precisament per això: pots executar-ho tot sense descarregar res. A 11-07 veuràs l'estàndard real de l'ecosistema.

Obtenir un Logger:

package com.nexussoftware.bibliotech.servei;

import java.util.logging.Logger;

public class Cataleg {

    /**
     * Un Logger per classe, estatic i final, anomenat amb el nom complet
     * de la classe. Es la convencio universal, i permet filtrar per paquet:
     * activar el nivell FINE per a com.nexussoftware.bibliotech.servei i deixar
     * la resta a INFO.
     */
    private static final Logger LOG = Logger.getLogger(Cataleg.class.getName());

    // ...
}

Per què static final: perquè el Logger no depèn de la instància, obtenir-lo és relativament car i així es fa una sola vegada per classe.

Registrar missatges:

LOG.severe("Missatge greu");
LOG.warning("Avis");
LOG.info("Informacio");
LOG.config("Configuracio");
LOG.fine("Detall de depuracio");

// Forma general, amb el nivell explicit
LOG.log(Level.INFO, "Missatge");

// AMB EXCEPCIO: la forma correcta de registrar una fallada
LOG.log(Level.SEVERE, "Missatge descriptiu", excepcio);

La forma mandrosa amb Supplier (Java 8+), que és important:

// MALAMENT: la concatenacio es fa SEMPRE, encara que FINE estigui desactivat
LOG.fine("Cercant " + referencia + " en un cataleg de " + materials.size()
        + " materials, index amb " + index.size() + " entrades");

// BE: la lambda nomes s'avalua si el nivell FINE esta actiu
LOG.fine(() -> "Cercant " + referencia + " en un cataleg de " + materials.size()
        + " materials, index amb " + index.size() + " entrades");

És el mateix principi d'avaluació mandrosa de les lambdes de 04-05 i de l'Objects.requireNonNull amb Supplier de 06-03. En un mètode molt cridat, la diferència entre construir aquella cadena un milió de vegades i no construir-la cap és perfectament mesurable.

L'alternativa clàssica, abans de les lambdes, era comprovar abans:

if (LOG.isLoggable(Level.FINE)) {
    LOG.fine("Missatge car: " + calcularDiagnostic());
}

Continua sent vàlida i de vegades necessària —quan el càlcul del missatge és car de debò—, però la forma amb Supplier és més neta.

La jerarquia de loggers. Els Logger s'organitzen pel seu nom, separat per punts, formant un arbre:

flowchart TB
    R["'' (arrel)<br/>nivell INFO, ConsoleHandler"]
    R --> C["com"]
    C --> N["com.nexussoftware"]
    N --> B["com.nexussoftware.bibliotech"]
    B --> D["...bibliotech.domini"]
    B --> S["...bibliotech.servei<br/>nivell FINE"]
    B --> P["...bibliotech.presentacio"]
    S --> S1["...servei.Cataleg"]
    S --> S2["...servei.GestorPrestecs"]

Un Logger sense nivell propi hereta el del seu pare, i els seus missatges es propaguen als Handler de tots els seus ancestres. Això és el que permet configurar amb precisió:

// Activar el detall NOMES a la capa de servei
Logger.getLogger("com.nexussoftware.bibliotech.servei").setLevel(Level.FINE);
// La resta continua a INFO

  1. Handler, Formatter i logging.properties

Tres peces que cal distingir:

Peça Què fa
Logger Rep els missatges i decideix si passen el filtre de nivell
Handler Els envia a una destinació: consola, fitxer, socket, memòria
Formatter Converteix el registre en text
Filter Criteri addicional més fi que el nivell

Els Handler inclosos al JDK:

Handler Destinació
ConsoleHandler System.err
FileHandler Un fitxer, amb rotació per mida i nombre
StreamHandler Un OutputStream qualsevol
SocketHandler Un host i port remots
MemoryHandler Una memòria intermèdia circular en memòria

Configuració programàtica completa per a BiblioTech:

package com.nexussoftware.bibliotech.presentacio;

import java.io.IOException;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

/**
 * Configuracio del sistema de registre de BiblioTech.
 *
 * Dues destinacions:
 *   - CONSOLA: nomes INFO i superiors, en format compacte d'una linia.
 *   - FITXER: tot des de FINE, amb rotacio, per a diagnostic posterior.
 */
public final class ConfiguracioLog {

    private static final String CAMI_LOG = "bibliotech-%g.log";   // %g = numero de rotacio
    private static final int MIDA_MAXIMA = 1_000_000;             // 1 MB per fitxer
    private static final int FITXERS_ROTACIO = 5;                 // 5 fitxers com a maxim

    private static boolean inicialitzat = false;

    private ConfiguracioLog() { }

    public static synchronized void inicialitzar() {
        if (inicialitzat) {
            return;                          // idempotent (06-06)
        }
        inicialitzat = true;

        Logger arrel = Logger.getLogger("");

        // 1. Treure els handlers per defecte: els substituim pels nostres
        for (Handler h : arrel.getHandlers()) {
            arrel.removeHandler(h);
        }

        // 2. El logger arrel accepta tot; cada handler filtra pel seu compte
        arrel.setLevel(Level.ALL);

        // 3. CONSOLA: nomes el que es important, format compacte
        ConsoleHandler consola = new ConsoleHandler();
        consola.setLevel(Level.INFO);
        consola.setFormatter(new FormatCompacte());
        arrel.addHandler(consola);

        // 4. FITXER: tot el detall, amb rotacio
        try {
            FileHandler fitxer = new FileHandler(CAMI_LOG, MIDA_MAXIMA, FITXERS_ROTACIO, true);
            fitxer.setLevel(Level.FINE);
            fitxer.setFormatter(new FormatDetallat());
            arrel.addHandler(fitxer);
        } catch (IOException e) {
            // Si no es pot crear el fitxer, l'aplicacio NO ha de caure:
            // es degrada a nomes consola i s'avisa. Degradacio elegant.
            arrel.log(Level.WARNING,
                    "No s'ha pogut crear el fitxer de log; nomes hi haura registre en consola", e);
        }

        // 5. Nivell especific per paquet
        Logger.getLogger("com.nexussoftware.bibliotech.servei").setLevel(Level.FINE);
        Logger.getLogger("com.nexussoftware.bibliotech.domini").setLevel(Level.INFO);

        Logger.getLogger(ConfiguracioLog.class.getName()).config("Sistema de registre inicialitzat");
    }

    /** Una linia per esdeveniment: nivell, classe curta i missatge. */
    private static class FormatCompacte extends Formatter {
        @Override
        public String format(LogRecord registre) {
            String classe = registre.getSourceClassName();
            String curta = (classe == null) ? "?" : classe.substring(classe.lastIndexOf('.') + 1);

            StringBuilder sb = new StringBuilder();
            sb.append(String.format("[%-7s] %-18s %s%n",
                    registre.getLevel().getName(), curta, formatMessage(registre)));

            if (registre.getThrown() != null) {
                sb.append("          causa: ").append(registre.getThrown()).append('\n');
            }
            return sb.toString();
        }
    }

    /** Format complet per al fitxer, amb marca de temps, fil i stack trace. */
    private static class FormatDetallat extends Formatter {
        @Override
        public String format(LogRecord registre) {
            StringBuilder sb = new StringBuilder();

            // Marca de temps. S'usa l'instant en millisegons i es formata
            // sense java.time, que es materia de 10-05.
            sb.append(String.format("%tF %<tT.%<tL", registre.getMillis()));
            sb.append(String.format(" [%-6d] %-7s ",
                    registre.getLongThreadID(), registre.getLevel().getName()));
            sb.append(registre.getSourceClassName()).append('.')
              .append(registre.getSourceMethodName()).append(" - ");
            sb.append(formatMessage(registre)).append('\n');

            // El stack trace complet, sagnat
            Throwable t = registre.getThrown();
            if (t != null) {
                sb.append("    ").append(t).append('\n');
                for (StackTraceElement marc : t.getStackTrace()) {
                    sb.append("        at ").append(marc).append('\n');
                }
                // Causes i suprimides (06-01, 06-06)
                Throwable causa = t.getCause();
                int nivell = 1;
                while (causa != null && nivell <= 5) {
                    sb.append("    Caused by: ").append(causa).append('\n');
                    causa = causa.getCause();
                    nivell++;
                }
                for (Throwable suprimida : t.getSuppressed()) {
                    sb.append("    Suppressed: ").append(suprimida).append('\n');
                }
            }
            return sb.toString();
        }
    }
}

Configuració per fitxer. L'alternativa a configurar en codi és un fitxer logging.properties, que s'activa en arrencar la JVM:

# logging.properties de BiblioTech

# Handlers globals
handlers = java.util.logging.ConsoleHandler, java.util.logging.FileHandler

# Nivell del logger arrel: deixa passar tot, els handlers filtren
.level = ALL

# --- Consola: nomes INFO i superiors ---
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# --- Fitxer: tot des de FINE, amb rotacio ---
java.util.logging.FileHandler.pattern = bibliotech-%g.log
java.util.logging.FileHandler.limit = 1000000
java.util.logging.FileHandler.count = 5
java.util.logging.FileHandler.append = true
java.util.logging.FileHandler.level = FINE
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

# Format de SimpleFormatter: data, nivell, classe.metode, missatge, excepcio
java.util.logging.SimpleFormatter.format = %1$tF %1$tT.%1$tL %4$-7s [%2$s] %5$s%6$s%n

# --- Nivells per paquet ---
com.nexussoftware.bibliotech.level = INFO
com.nexussoftware.bibliotech.servei.level = FINE
com.nexussoftware.bibliotech.domini.level = INFO

I s'arrenca així:

java -Djava.util.logging.config.file=logging.properties \
     -cp . com.nexussoftware.bibliotech.presentacio.BiblioTechApp

L'avantatge decisiu de la configuració per fitxer: es pot canviar el nivell de log sense recompilar ni tocar el codi. En producció, quan cal investigar un problema, es puja el nivell a FINE al paquet afectat, es reprodueix, i es torna a baixar.

  1. Registrar una excepció correctament

La forma correcta i les incorrectes, l'una al costat de l'altra:

try {
    gestor.prestar(referencia, idEmpleat, dia);

} catch (BiblioTechException e) {

    // ==================== CORRECTE ====================
    // El Throwable va com a TERCER argument: el Formatter imprimeix el stack
    // trace complet, amb les seves causes i suprimides, al mateix esdeveniment de log.
    LOG.log(Level.WARNING, "No s'ha pogut prestar " + referencia + " a " + idEmpleat, e);

    // ==================== INCORRECTE ====================

    // 1. printStackTrace: va a System.err, sense nivell, sense data, sense filtre
    e.printStackTrace();

    // 2. Nomes el missatge: perd el stack trace i totes les causes
    LOG.warning("Error: " + e.getMessage());

    // 3. Concatenar l'excepcio: dona nomes toString(), sense pila
    LOG.warning("Error: " + e);

    // 4. Registrar I rellancar: la mateixa fallada apareixera dues vegades
    LOG.log(Level.SEVERE, "Error", e);
    throw e;
}

Sobre el quart cas, que mereix la seva pròpia regla:

Registra on gestiones. Si vas a rellançar, no registris el stack trace complet: la capa que finalment gestioni l'error ho farà, i amb més context. Com a molt, afegeix una línia de context a nivell FINE.

Si cada capa registra i rellança, una sola fallada produeix cinc esdeveniments SEVERE amb cinc stack traces gairebé idèntics, i trobar el primer es converteix en arqueologia.

I una advertència sobre el nivell: una excepció capturada i gestionada correctament poques vegades és SEVERE. El MaterialNoTrobatException que es produeix perquè l'usuari va escriure malament una referència és INFO o FINE, no un error del sistema. Reserva SEVERE per al que requereix que algú intervingui.

  1. Què NO registrar mai

Un log és un fitxer que es copia, s'envia per correu, s'agrega a un sistema centralitzat i es guarda mesos. Tot el que hi escriguis surt del control de la teva aplicació.

Mai registris:

Categoria Exemples
Credencials Contrasenyes, fins i tot xifrades o "només els primers caràcters"
Tokens i claus Tokens de sessió, claus d'API, galetes d'autenticació
Dades financeres Números de targeta, CVV, comptes bancaris
Dades personals sensibles DNI, adreça, salut, dades biomètriques
Contingut complet de peticions Pot dur qualsevol dels anteriors a dins
Bolcats d'objectes sencers LOG.fine("Usuari: " + usuari) imprimirà el que hi hagi al seu toString(), avui i d'aquí a dos anys

Aquell últim punt és el més traïdor, perquè la fallada apareix després:

// Avui: Empleat.toString() retorna "EMP-001 (Marta Ruiz)". Sembla inofensiu.
LOG.fine(() -> "Processant " + empleat);

// D'aqui a un any algu afegeix el DNI i el telefon al toString()...
// i de sobte estas registrant dades personals a tots els fitxers de log,
// sense que ningu hagi tocat aquesta linia.

La defensa: registra camps concrets, no objectes.

LOG.fine(() -> "Processant empleat " + empleat.getIdentificador());

Quan necessitis registrar alguna cosa parcialment identificable, emmascara-la:

/** Deixa visibles els ultims 4 caracters: 978-0000000001 -> ***0001 */
private static String emmascarar(String valor) {
    if (valor == null || valor.length() <= 4) {
        return "****";
    }
    return "***" + valor.substring(valor.length() - 4);
}

Advertència formal: en un sistema real, quines dades personals es poden registrar, durant quant de temps i amb quines mesures de protecció no és una decisió tècnica. Està regulat —a la Unió Europea, pel RGPD— i ho ha de revisar el responsable de protecció de dades o el departament de compliment normatiu de la teva organització. Davant del dubte, no ho registris i pregunta. Un log amb dades personals mal gestionades és un incident de seguretat i pot comportar sancions.

A BiblioTech, EMP-001 és un identificador intern perfectament registrable; el nom complet de la persona ja és una dada personal, i en un sistema real caldria consultar-ho abans de posar-lo al log.

  1. Missatges de log útils i correlació

Un missatge de log respon a cinc preguntes: què ha passat, amb quines dades, en quina operació, amb quin resultat i —si ha fallat— per què.

Missatge Valoració
"Error" Inútil
"Error en prestar" Falta tot el context
"Error en prestar LIB-0001" Falta qui i quan
"Prestec rebutjat: LIB-0001 a EMP-001 el dia 12. Motiu: material no disponible fins al dia 27" Complet

Els identificadors que sempre han d'aparèixer a BiblioTech:

  • Referència del material (LIB-0001) i referència del préstec (PR-0007).
  • Identificador de l'empleat (EMP-001), no el seu nom.
  • Dia de l'operació.
  • Identificador d'operació, per correlacionar diverses línies del mateix flux.

Sobre aquest últim punt: en un sistema amb moltes operacions concurrents, les línies de log de fluxos diferents s'entrellacen i no hi ha manera de saber quines pertanyen al mateix. La solució és un identificador de correlació que es propagui per totes les capes i aparegui a cada missatge:

LOG.info(() -> "[" + idOperacio + "] Prestec " + referenciaPrestec + " completat");

Amb ell, filtrar per INC-A3F91C0B al fitxer retorna la història completa d'aquella operació, de principi a fi, encara que n'hi hagués cent més en paral·lel. Els sistemes moderns ho automatitzen amb el MDC de SLF4J o amb traçabilitat distribuïda (OpenTelemetry), i el log estructurat —esdeveniments en JSON amb camps anomenats en comptes de text lliure— permet a més consultar-los com si fossin una base de dades. Tots dos temes es recuperen a 11-07 i a 12-07.

  1. L'ecosistema real: SLF4J, Logback i Log4j2

java.util.logging és el que has fet servir aquí perquè ve al JDK i no requereix instal·lar res. Però en un projecte professional et trobaràs una altra cosa.

Component Què és
SLF4J Una façana: una API de logging sense implementació. El teu codi depèn només d'ella
Logback La implementació més usada, del mateix autor que SLF4J. La que porta Spring Boot per defecte
Log4j2 L'altra gran implementació, amb molt bon rendiment gràcies al seu registre asíncron
java.util.logging La del JDK. Menys flexible, però sense dependències

Per què existeix la façana: el teu codi escriu contra SLF4J, i en el moment del desplegament es tria la implementació. Si demà canvies de Logback a Log4j2, no toques ni una línia del teu codi: canvies una dependència. És exactament el principi de programar contra interfícies de 04-01, aplicat a les llibreries.

Així es veu el mateix codi amb SLF4J:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Cataleg {

    private static final Logger LOG = LoggerFactory.getLogger(Cataleg.class);

    public void registrar(Material material) {
        // Sintaxi de marcadors {}: sense concatenacio i sense lambda.
        // El missatge nomes es compon si el nivell esta actiu.
        LOG.debug("Registrant {} en un cataleg de {} materials",
                  material.getReferencia(), materials.size());
        // ...
    }
}

Els avantatges enfront de JUL: la sintaxi de marcadors {} resol el problema de l'avaluació mandrosa sense lambdes, la configuració en XML o YAML és molt més expressiva, hi ha appenders per a totes les destinacions imaginables, i el suport de log estructurat en JSON ve de sèrie.

Tot el que has après en aquesta lliçó es trasllada tal qual: els nivells, un logger per classe, registrar l'excepció com a argument i no concatenar-la, no registrar dades sensibles, registrar on gestiones. Només canvia l'API. Les llibreries essencials de l'ecosistema, inclosa aquesta, es veuen a 11-07.

  1. BiblioTech: la refactorització final

Ara tot junt. Primer, el Cataleg amb logging en comptes de println:

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

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

/**
 * Cataleg de BiblioTech, versio final del modul 6.
 *
 * Canvis respecte a 06-04:
 *   - Zero System.out.println: tot el diagnostic va al Logger.
 *   - Els missatges de log porten identificadors, no noms de persones.
 *   - Els missatges cars usen Supplier per no construir-se si el nivell esta desactivat.
 */
public class Cataleg {

    private static final Logger LOG = Logger.getLogger(Cataleg.class.getName());

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

    private String propietariDelBloqueig = null;

    public void registrar(Material material) {
        Objects.requireNonNull(material, "El material a registrar no pot ser nul");
        String referencia = material.getReferencia();

        // FINE: detall de depuracio, desactivat en produccio.
        // Amb Supplier, la concatenacio no s'executa si FINE esta apagat.
        LOG.fine(() -> "Registrant " + referencia + " (cataleg actual: "
                + materials.size() + " materials)");

        Material existent = indexPerReferencia.get(referencia);
        if (existent != null) {
            // Es registra a nivell FINE i es LLANCA. No es registra a nivell superior
            // perque qui capturi decidira la seva gravetat: en una importacio massiva
            // un duplicat es rutina, en una alta manual es un avis.
            LOG.fine(() -> "Alta rebutjada: referencia " + referencia + " duplicada");
            throw new ReferenciaDuplicadaException(
                    ReferenciaDuplicadaException.Tipus.REFERENCIA, referencia,
                    existent.getTitol());
        }

        if (material instanceof Llibre llibre) {
            String titolExistent = titolsPerIsbn.get(llibre.getIsbn());
            if (titolExistent != null) {
                LOG.fine(() -> "Alta rebutjada: ISBN duplicat a " + referencia);
                throw new ReferenciaDuplicadaException(
                        ReferenciaDuplicadaException.Tipus.ISBN, llibre.getIsbn(), titolExistent);
            }
            titolsPerIsbn.put(llibre.getIsbn(), llibre.getTitol());
        }

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

        // INFO: fita de negoci. Es llegeix com la historia del que fa l'aplicacio.
        LOG.info(() -> "Material registrat: " + referencia
                + " (total " + materials.size() + ")");
    }

    public Material obtenirPerReferencia(String referencia) {
        Objects.requireNonNull(referencia, "La referencia no pot ser nulla");

        Material trobat = indexPerReferencia.get(referencia);
        if (trobat == null) {
            LOG.fine(() -> "Referencia no trobada: " + referencia);
            throw new MaterialNoTrobatException(referencia, materials.size());
        }
        return trobat;
    }

    public Optional<Material> cercarPerReferencia(String referencia) {
        if (referencia == null) { return Optional.empty(); }
        return Optional.ofNullable(indexPerReferencia.get(referencia));
    }

    public void bloquejar(String idPropietari) {
        if (propietariDelBloqueig != null && !propietariDelBloqueig.equals(idPropietari)) {
            LOG.warning(() -> "Bloqueig denegat a " + idPropietari
                    + ": el te " + propietariDelBloqueig);
            throw new IllegalStateException(
                    "El cataleg esta bloquejat per " + propietariDelBloqueig);
        }
        propietariDelBloqueig = idPropietari;
        LOG.fine(() -> "Bloqueig del cataleg adquirit per " + idPropietari);
    }

    public void desbloquejar(String idPropietari) {
        if (!Objects.equals(propietariDelBloqueig, idPropietari)) {
            // WARNING: passa alguna cosa rara, pero no impedeix continuar
            LOG.warning(() -> "Intent de desbloqueig per " + idPropietari
                    + " sense ser el propietari (" + propietariDelBloqueig + ")");
            return;
        }
        propietariDelBloqueig = null;
        LOG.fine(() -> "Bloqueig del cataleg alliberat per " + idPropietari);
    }

    public boolean estaBloquejat()     { return propietariDelBloqueig != null; }
    public boolean existeix(String r)  { return r != null && indexPerReferencia.containsKey(r); }
    public int mida()                  { return materials.size(); }
    public List<Material> llistar()    { return List.copyOf(materials); }
}

Ara GestorPrestecs, amb logging, identificador d'operació i compensació:

package com.nexussoftware.bibliotech.servei;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.nexussoftware.bibliotech.domini.*;

/** Orquestra prestecs amb registre complet i estat coherent. */
public class GestorPrestecs {

    private static final Logger LOG = Logger.getLogger(GestorPrestecs.class.getName());

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

    private int comptador = 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");
        empleats.put(empleat.getIdentificador(), empleat);
        // Es registra l'IDENTIFICADOR, no el nom: el nom es una dada personal
        LOG.info(() -> "Empleat donat d'alta: " + empleat.getIdentificador());
    }

    public Prestec prestar(String idOperacio, String referencia, String idEmpleat, int dia) {
        Objects.requireNonNull(idOperacio, "L'identificador d'operacio no pot ser nul");
        Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
        Objects.requireNonNull(idEmpleat, "L'identificador d'empleat no pot ser nul");
        if (dia < 1) {
            throw new IllegalArgumentException("El dia ha de ser 1 o posterior, i era: " + dia);
        }

        LOG.fine(() -> "[" + idOperacio + "] Iniciant prestec de " + referencia
                + " a " + idEmpleat + " el dia " + dia);

        // --- Fase 1: lectura i validacio (no modifica res) ---
        Material material = cataleg.obtenirPerReferencia(referencia);
        Empleat empleat = empleats.get(idEmpleat);
        if (empleat == null) {
            throw new MaterialNoTrobatException(idEmpleat, empleats.size());
        }
        if (!material.estaDisponible()) {
            throw new MaterialNoDisponibleException(referencia, material.getTitol(),
                    "un altre empleat", dia + Prestec.DIES_PRESTEC);
        }
        if (!empleat.potPrendrePrestat()) {
            throw new LimitPrestecsExceditException(idEmpleat, empleat.getNom(),
                    empleat.getPrestecsAcumulats(), Empleat.MAX_PRESTECS_SIMULTANIS);
        }

        // --- Fase 2: modificacio amb compensacio (06-05) ---
        boolean materialMarcat = false;
        boolean quotaConsumida = false;
        boolean completat = false;
        String referenciaPrestec = seguentReferencia();

        try {
            material.prestar();
            materialMarcat = true;

            empleat.registrarPrestec();
            quotaConsumida = true;

            Prestec prestec = new Prestec(referenciaPrestec, material, empleat, dia);
            prestecs.put(referenciaPrestec, prestec);
            completat = true;

            LOG.info(() -> "[" + idOperacio + "] Prestec " + referenciaPrestec
                    + " registrat: " + referencia + " -> " + idEmpleat + " (dia " + dia + ")");
            return prestec;

        } finally {
            if (!completat) {
                // WARNING, no SEVERE: la compensacio es el comportament previst
                LOG.warning(() -> "[" + idOperacio + "] Prestec incomplet de " + referencia
                        + "; compensant");
                try {
                    if (quotaConsumida)  { empleat.registrarDevolucio(); }
                    if (materialMarcat)  { material.retornar(); }
                    LOG.fine(() -> "[" + idOperacio + "] Estat restaurat");
                } catch (RuntimeException fallada) {
                    // SEVERE de debo: la compensacio ha fallat i l'estat pot
                    // haver quedat incoherent. Aixo SI que requereix intervencio.
                    LOG.log(Level.SEVERE, "[" + idOperacio
                            + "] LA COMPENSACIO HA FALLAT. Estat possiblement incoherent a "
                            + referencia, fallada);
                }
            }
        }
    }

    public void retornar(String idOperacio, String referenciaPrestec, int dia) {
        Prestec prestec = prestecs.get(referenciaPrestec);
        if (prestec == null) {
            throw new MaterialNoTrobatException(referenciaPrestec, prestecs.size());
        }
        if (prestec.estaRetornat()) {
            throw new PrestecJaRetornatException(referenciaPrestec, prestec.getDiaInici());
        }

        prestec.registrarDevolucio(dia);
        prestec.getTitular().registrarDevolucio();

        int retard = prestec.calcularDiesRetard(dia);
        if (retard > 0) {
            LOG.info(() -> "[" + idOperacio + "] Devolucio de " + referenciaPrestec
                    + " amb " + retard + " dies de retard");
        } else {
            LOG.info(() -> "[" + idOperacio + "] Devolucio de " + referenciaPrestec + " en termini");
        }
    }

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

    public int prestecsRegistrats() { return prestecs.size(); }
}

I la capa de presentació, que és on es captura i es tradueix:

package com.nexussoftware.bibliotech.presentacio;

import java.util.logging.Level;
import java.util.logging.Logger;

import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.servei.GestorPrestecs;

/**
 * Capa de presentacio: la UNICA que captura per a l'usuari, l'unica que
 * imprimeix per consola el que l'usuari ha demanat veure, i l'unica que decideix
 * quin missatge mostrar en cada cas.
 */
public class OperacionsConsola {

    private static final Logger LOG = Logger.getLogger(OperacionsConsola.class.getName());

    private final GestorPrestecs gestor;
    private int comptadorOperacions = 0;

    public OperacionsConsola(GestorPrestecs gestor) {
        this.gestor = gestor;
    }

    /**
     * Executa un prestec traduint cada fallada a un missatge comprensible.
     *
     * Aqui SI que es captura: es la frontera amb l'usuari i hi ha decisions a prendre.
     */
    public void prestar(String referencia, String idEmpleat, int dia) {
        String idOperacio = seguentIdOperacio();

        try {
            gestor.prestar(idOperacio, referencia, idEmpleat, dia);
            // SORTIDA del programa, no log: es el que l'usuari ha demanat veure
            System.out.println("Prestat " + referencia + " a " + idEmpleat
                    + ". Devolucio prevista: dia " + (dia + Prestec.DIES_PRESTEC));

        } catch (MaterialNoTrobatException e) {
            // Fallada esperada i corregible: missatge amable, log de baix nivell
            System.out.println("No existeix el material '" + e.getReferenciaCercada() + "'.");
            if (e.catalegBuit()) {
                System.out.println("El cataleg esta buit: carregueu primer els materials.");
            }
            LOG.fine(() -> "[" + idOperacio + "] Referencia inexistent: "
                    + e.getReferenciaCercada());

        } catch (MaterialNoDisponibleException e) {
            System.out.println("'" + e.getTitol() + "' esta prestat.");
            System.out.println("Disponible previsiblement en " + e.diesEspera(dia) + " dies.");
            System.out.println("El podeu reservar amb l'opcio 5.");
            LOG.fine(() -> "[" + idOperacio + "] Material no disponible: " + e.getReferencia());

        } catch (LimitPrestecsExceditException e) {
            System.out.printf("Heu assolit el limit de %d prestecs simultanis.%n",
                    e.getLimit());
            System.out.printf("Heu de retornar %d material(s) abans de prendre'n un altre.%n",
                    e.devolucionsNecessaries());
            LOG.fine(() -> "[" + idOperacio + "] Limit excedit per " + e.getIdEmpleat());

        } catch (BiblioTechException e) {
            // Xarxa de seguretat del domini: capturable gracies a l'arrel comuna (06-04)
            System.out.println("No s'ha pogut completar l'operacio: " + e.getMessage());
            LOG.log(Level.WARNING, "[" + idOperacio + "] Fallada de domini no prevista ["
                    + e.getCodi() + "]", e);

        } catch (RuntimeException e) {
            // BUG: l'usuari no veu els detalls, el registre els veu tots
            System.out.println("S'ha produit un error intern.");
            System.out.println("Incidencia: " + idOperacio);
            LOG.log(Level.SEVERE, "[" + idOperacio + "] Error no controlat en prestar "
                    + referencia + " a " + idEmpleat, e);
        }
    }

    private String seguentIdOperacio() {
        comptadorOperacions++;
        return String.format("OP-%05d", comptadorOperacions);
    }

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

    public static void main(String[] args) {
        ConfiguracioLog.inicialitzar();

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

        OperacionsConsola consola = new OperacionsConsola(gestor);

        System.out.println("\n----- Operacions -----");
        consola.prestar("LIB-0001", "EMP-001", 10);
        consola.prestar("LIB-0001", "EMP-002", 11);    // no disponible
        consola.prestar("LIB-9999", "EMP-001", 11);    // no trobat
        consola.prestar("LIB-0002", "EMP-001", 11);
        consola.prestar("LIB-0003", "EMP-001", 11);
        consola.prestar("LIB-0002", "EMP-001", 12);    // no disponible (ja el te)
    }
}

Sortida en consola (nivell INFO i superiors del log, més la sortida del programa):

[INFO   ] Cataleg            Material registrat: LIB-0001 (total 1)
[INFO   ] Cataleg            Material registrat: LIB-0002 (total 2)
[INFO   ] Cataleg            Material registrat: LIB-0003 (total 3)
[INFO   ] GestorPrestecs     Empleat donat d'alta: EMP-001
[INFO   ] GestorPrestecs     Empleat donat d'alta: EMP-002

----- Operacions -----
[INFO   ] GestorPrestecs     [OP-00001] Prestec PR-0001 registrat: LIB-0001 -> EMP-001 (dia 10)
Prestat LIB-0001 a EMP-001. Devolucio prevista: dia 25
'Java Eficac' esta prestat.
Disponible previsiblement en 14 dies.
El podeu reservar amb l'opcio 5.
No existeix el material 'LIB-9999'.
[INFO   ] GestorPrestecs     [OP-00004] Prestec PR-0002 registrat: LIB-0002 -> EMP-001 (dia 11)
Prestat LIB-0002 a EMP-001. Devolucio prevista: dia 26
[INFO   ] GestorPrestecs     [OP-00005] Prestec PR-0003 registrat: LIB-0003 -> EMP-001 (dia 11)
Prestat LIB-0003 a EMP-001. Devolucio prevista: dia 26
'Patrons de Disseny' esta prestat.
Disponible previsiblement en 14 dies.
El podeu reservar amb l'opcio 5.

I a bibliotech-0.log, amb tot el detall:

2026-08-05 11:42:07.104 [1     ] CONFIG  ...ConfiguracioLog.inicialitzar - Sistema de registre inicialitzat
2026-08-05 11:42:07.118 [1     ] FINE    ...Cataleg.registrar - Registrant LIB-0001 (cataleg actual: 0 materials)
2026-08-05 11:42:07.119 [1     ] INFO    ...Cataleg.registrar - Material registrat: LIB-0001 (total 1)
...
2026-08-05 11:42:07.201 [1     ] FINE    ...GestorPrestecs.prestar - [OP-00002] Iniciant prestec de LIB-0001 a EMP-002 el dia 11
2026-08-05 11:42:07.202 [1     ] FINE    ...OperacionsConsola.prestar - [OP-00002] Material no disponible: LIB-0001
2026-08-05 11:42:07.205 [1     ] FINE    ...Cataleg.obtenirPerReferencia - Referencia no trobada: LIB-9999
2026-08-05 11:42:07.206 [1     ] FINE    ...OperacionsConsola.prestar - [OP-00003] Referencia inexistent: LIB-9999

Compara la consola i el fitxer. La consola mostra el que l'usuari necessita: missatges clars i les fites importants. El fitxer conté la història completa, amb marca de temps, fil, classe, mètode i identificador d'operació, filtrable per qualsevol d'aquells camps. I no hi ha un sol System.out.println de diagnòstic a tot el projecte.

Errors Comuns i Consells

Capturar on es produeix en comptes d'on es decideix. Un catch que només imprimeix i retorna null degrada una excepció amb nom i dades al null mut que va costar dues lliçons eliminar. Si no pots decidir, no capturis.

Mostrar un stack trace a l'usuari. És un risc de seguretat —revela paquets, versions i camins—, no li serveix de res i destrueix la confiança. Missatge clar més identificador d'incidència; el trace, al registre.

Registrar i rellançar a cada capa. Una fallada apareix cinc vegades amb cinc traces gairebé idèntiques. Registra on gestiones.

Registrar-ho tot com a SEVERE. Quan tot és un error, ningú no mira els errors. SEVERE és només per al que requereix que algú intervingui.

Concatenar a les crides de log de nivell baix. LOG.fine("... " + x + " ...") construeix la cadena encara que FINE estigui desactivat. Fes servir la forma amb Supplier: LOG.fine(() -> "...").

Registrar objectes complets. LOG.fine("Empleat: " + empleat) imprimirà el que retorni toString() avui i d'aquí a dos anys, quan algú hi afegeixi el DNI. Registra camps concrets.

Registrar credencials, tokens o dades personals. Els logs es copien, s'envien i s'arxiven durant mesos. És un incident de seguretat, i en un sistema real la política la marca el responsable de protecció de dades, no tu.

printStackTrace() en producció. Va a System.err sense nivell, sense data, sense filtre i sense context. Fes servir LOG.log(Level.X, "missatge", e).

Reintentar operacions no idempotents. Si el préstec es va registrar i només va fallar la confirmació, el reintent crea un segon préstec. Només es reintenta el que es pot repetir sense efectes secundaris.

Confiar en la validació prèvia amb recursos compartits. Entre l'exists() i l'open() el fitxer pot desaparèixer. És TOCTOU, i l'excepció cal gestionar-la de totes maneres.

Un Logger no estàtic o creat a cada crida. Obtenir-lo té cost. private static final Logger LOG = Logger.getLogger(LaMevaClasse.class.getName());, una vegada per classe.

Consell: fes servir un identificador d'operació a tots els missatges d'un mateix flux. Converteix un log il·legible en una història que es pot seguir amb un sol filtre.

Consell: llegeix el teu propi log com si fossis el de guàrdia a les tres de la matinada. Si no pots reconstruir què ha passat, falten dades. Si hi ha tant soroll que no trobes res, sobren línies a INFO.

Consell: configura per fitxer, no en codi. Poder pujar el nivell a FINE en un paquet concret sense recompilar és el que salva una investigació en producció.

Consell: en passar a SLF4J a 11-07, tot això es trasllada tal qual. Només canvia l'API; els criteris són els mateixos.

Exercicis

Exercici 1: la política d'errors de cada capa

Per a cadascun dels vuit escenaris, respon: (a) en quina capa es detecta, (b) en quina capa s'ha de capturar, (c) quin nivell de log correspon, (d) quin missatge exacte veu l'usuari i (e) quin missatge exacte va al registre. Justifica cada decisió.

  1. La Marta escriu "dotze" on es demanava el nombre de dies.
  2. Llibre rep anyPublicacio = 1200 al seu constructor.
  3. El fitxer cataleg.txt no existeix en arrencar.
  4. Un NullPointerException a Prestec.descripcioTitular perquè el titular és null.
  5. El Diego intenta el seu quart préstec simultani.
  6. El servei d'avisos no respon en notificar un venciment.
  7. El disc s'omple mentre s'exporta el catàleg.
  8. OutOfMemoryError durant una importació massiva.

Després, escriu la classe PoliticaErrors amb un mètode String decidir(Throwable t) que, donat un Throwable, retorni una recomanació amb el nivell de log, si el missatge és apte per a l'usuari i l'acció suggerida. Recolza't en BiblioTechException.esCorregiblePerUsuari().

Exercici 2: RegistreOperacions amb java.util.logging

Escriu RegistreOperacions a com.nexussoftware.bibliotech.servei, que embolcalli qualsevol operació de BiblioTech aportant registre complet i mesurament.

Requisits:

  • <T> T executar(String nomOperacio, String idEmpleat, Supplier<T> operacio):
    • Genera un identificador d'operació OP-NNNNN.
    • Registra a FINE l'inici amb l'identificador, el nom i l'empleat.
    • Mesura la durada amb System.nanoTime().
    • Si té èxit: registra a INFO el final amb la durada en mil·lisegons.
    • Si llança una BiblioTechException: la registra a WARNING amb l'excepció com a tercer argument i la rellança.
    • Si llança qualsevol altra RuntimeException: la registra a SEVERE amb l'identificador com a incidència i l'embolcalla en una IllegalStateException el missatge de la qual inclogui la incidència, conservant la causa.
    • Fes servir try-finally perquè la durada es registri també quan falla.
  • Un mètode void bolcarEstadistiques() que mostri, per nom d'operació: nombre d'execucions, èxits, fallades i durada mitjana.
  • Tots els missatges han de fer servir la forma mandrosa amb Supplier.
  • No ha de registrar el nom de cap empleat, només el seu identificador.

Al main, configura el logging amb dos handlers (consola a INFO, fitxer a FINE), executa almenys sis operacions —correctes, amb fallada de domini i amb bug— i mostra les estadístiques.

Exercici 3: auditoria de gestió d'errors

Aquest és el MenuBiblioTech que ha sobreviscut a tot el mòdul sense revisar-se. Té deu problemes de gestió d'errors i de logging. Troba'ls, explica el mal de cadascun i reescriu la classe aplicant tot el que has après al mòdul.

public class MenuBiblioTech {

    public void arrencar() {
        while (true) {
            try {
                System.out.print("Opcio: ");
                int opcio = Integer.parseInt(new Scanner(System.in).nextLine());
                switch (opcio) {
                    case 1: prestar(); break;
                    case 2: retornar(); break;
                    case 0: System.exit(0);
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
    }

    private void prestar() throws Exception {
        Scanner sc = new Scanner(System.in);
        System.out.print("Referencia: ");
        String ref = sc.nextLine();
        System.out.print("Empleat: ");
        String emp = sc.nextLine();

        try {
            gestor.prestar(ref, emp, 1);
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
            System.out.println(e.getStackTrace()[0]);
            throw e;
        } finally {
            sc.close();
        }
    }

    private void retornar() {
        try {
            gestor.retornar(llegirReferencia(), 1);
        } catch (Exception e) {
        }
        System.out.println("Devolucio processada");
    }
}

Pista sobre dos d'ells que són fàcils de passar per alt: un té a veure amb on es crea el Scanner i un altre amb el que l'últim mètode afirma a l'usuari.

Solucions

Solució 1

Taula de decisions:

# Escenari (a) Es detecta (b) Es captura (c) Nivell (d) Usuari (e) Registre
1 "dotze" com a nombre de dies Presentació (parseInt) Presentació FINE "'dotze' no es un numero. Escriviu un enter entre 1 i 30." [OP-00012] Entrada no numerica a dies: 'dotze'
2 anyPublicacio = 1200 Domini (constructor) Presentació o importador WARNING en lot, FINE en alta manual "L'any ha d'estar entre 1450 i 2100. Heu escrit 1200." Alta rebutjada de LIB-0007: any 1200 fora de rang
3 No existeix cataleg.txt Persistència main/arrencada WARNING "No hi havia cataleg previ. Es comenca de zero." No s'ha pogut carregar el cataleg des de 'cataleg.txt' + stack trace
4 NullPointerException per titular nul Domini Frontera SEVERE "Error intern. Incidencia INC-A3F91C0B." [INC-A3F91C0B] Error no controlat en emetre rebut + trace complet
5 Quart préstec del Diego Domini (Empleat) Presentació FINE "Heu assolit el limit de 3 prestecs. Retorneu 1 material." [OP-00018] Limit excedit per EMP-002 (3/3)
6 Servei d'avisos no respon Servei Servei (degradació) WARNING (res, o "els avisos s'enviaran més tard") Servei d'avisos no disponible; 3 avisos en cua + trace
7 Disc ple en exportar Persistència (IOException) Presentació SEVERE "No s'ha pogut exportar el cataleg. Comproveu l'espai al disc." Fallada d'E/S en exportar a 'cataleg-export.txt' + trace
8 OutOfMemoryError JVM Només la frontera SEVERE "Error irrecuperable. L'aplicacio es tancara." Error irrecuperable + classe i trace, amb el registre protegit

Justificacions dels casos menys evidents:

Cas 2: el nivell depèn del context, no del tipus. En una importació massiva, un any invàlid en una línia de mil és rutina i mereix WARNING a l'informe agregat; en una alta manual és un error de l'usuari i n'hi ha prou amb FINE. És la mateixa excepció amb dues polítiques, com a l'exercici 3 de 06-02.

Cas 4: un NullPointerException és sempre un bug, mai una cosa que l'usuari pugui corregir. D'aquí SEVERE i el missatge genèric amb incidència: l'usuari no ha de veure Prestec.java:97 ni saber que existeix una classe Prestec.

Cas 6: es captura a servei, no a presentació, perquè és allà on hi ha el coneixement per degradar: acumular els avisos en cua i continuar. La presentació no té per què assabentar-se que existeix un servei d'avisos.

Cas 7: SEVERE encara que l'usuari pugui actuar, perquè un disc ple afecta tot el sistema i algú d'operacions se n'ha d'assabentar.

Cas 8: l'únic cas en què es captura un Error, i només a la frontera, només per deixar constància abans de morir, i amb el propi registre protegit perquè escriure el log també pot fallar sense memòria.

package com.nexussoftware.bibliotech.servei;

import java.io.IOException;
import java.util.NoSuchElementException;
import java.util.logging.Level;

import com.nexussoftware.bibliotech.domini.BiblioTechException;

/**
 * Classifica un Throwable i recomana com tractar-lo.
 * Materialitza la taula de decisions d'aquesta llico.
 */
public final class PoliticaErrors {

    /** Recomanacio completa per a una fallada concreta (record, 04-07). */
    public record Recomanacio(Level nivell, boolean missatgeAptePerUsuari,
                              boolean requereixIncidencia, String accio) {

        @Override
        public String toString() {
            return String.format("nivell=%-7s | usuari=%-5s | incidencia=%-5s | %s",
                    nivell.getName(), missatgeAptePerUsuari, requereixIncidencia, accio);
        }
    }

    private PoliticaErrors() { }

    public static Recomanacio decidir(Throwable t) {
        if (t == null) {
            return new Recomanacio(Level.INFO, false, false, "Res a fer");
        }

        // 1. Error de la JVM: irrecuperable
        if (t instanceof Error) {
            return new Recomanacio(Level.SEVERE, false, true,
                    "Registrar (protegit) i acabar de manera ordenada. NO continuar.");
        }

        // 2. Excepcio del domini: fallada esperada, missatge ja escrit per a l'usuari
        if (t instanceof BiblioTechException bte) {
            if (bte.esCorregiblePerUsuari()) {
                return new Recomanacio(Level.FINE, true, false,
                        "Mostrar el missatge tal qual i permetre reintentar.");
            }
            return new Recomanacio(Level.WARNING, true, false,
                    "Mostrar el missatge i indicar que no depen de l'usuari.");
        }

        // 3. Fallades d'E/S: condicio de l'entorn
        if (t instanceof IOException) {
            return new Recomanacio(Level.SEVERE, false, true,
                    "Degradar si hi ha alternativa; si no, avortar l'operacio. Avisar operacions.");
        }

        // 4. Validacions tecniques frequents a la frontera amb l'usuari
        if (t instanceof NumberFormatException) {
            return new Recomanacio(Level.FINE, true, false,
                    "Demanar la dada de nou indicant el format esperat.");
        }
        if (t instanceof NoSuchElementException) {
            return new Recomanacio(Level.FINE, true, false,
                    "Informar que no s'ha trobat i oferir alternatives.");
        }

        // 5. Fallades de programacio: l'usuari no ha de veure detalls
        if (t instanceof NullPointerException
                || t instanceof IndexOutOfBoundsException
                || t instanceof ClassCastException) {
            return new Recomanacio(Level.SEVERE, false, true,
                    "BUG: missatge generic amb incidencia, stack trace complet al registre.");
        }

        // 6. Resta de no comprovades: probablement tambe un bug
        if (t instanceof RuntimeException) {
            return new Recomanacio(Level.SEVERE, false, true,
                    "Tractar com a bug fins que es demostri el contrari.");
        }

        // 7. Comprovades no contemplades
        return new Recomanacio(Level.WARNING, false, true,
                "Condicio externa: registrar amb detall i decidir a la capa superior.");
    }

    public static void main(String[] args) {
        Throwable[] casos = {
                new NumberFormatException("For input string: \"dotze\""),
                new IllegalArgumentException("L'any ha d'estar entre 1450 i 2100, i era: 1200"),
                new IOException("cataleg.txt (No such file or directory)"),
                new NullPointerException("Cannot invoke \"Empleat.getNom()\""),
                new OutOfMemoryError("Java heap space"),
                new NoSuchElementException("No line found")
        };

        System.out.printf("%-32s %s%n", "EXCEPCIO", "RECOMANACIO");
        System.out.println("-".repeat(120));
        for (Throwable t : casos) {
            System.out.printf("%-32s %s%n", t.getClass().getSimpleName(), decidir(t));
        }
    }
}

Sortida:

EXCEPCIO                         RECOMANACIO
------------------------------------------------------------------------------------------------------------------------
NumberFormatException            nivell=FINE    | usuari=true  | incidencia=false | Demanar la dada de nou indicant el format esperat.
IllegalArgumentException         nivell=SEVERE  | usuari=false | incidencia=true  | Tractar com a bug fins que es demostri el contrari.
IOException                      nivell=SEVERE  | usuari=false | incidencia=true  | Degradar si hi ha alternativa; si no, avortar l'operacio. Avisar operacions.
NullPointerException             nivell=SEVERE  | usuari=false | incidencia=true  | BUG: missatge generic amb incidencia, stack trace complet al registre.
OutOfMemoryError                 nivell=SEVERE  | usuari=false | incidencia=true  | Registrar (protegit) i acabar de manera ordenada. NO continuar.
NoSuchElementException           nivell=FINE    | usuari=true  | incidencia=false | Informar que no s'ha trobat i oferir alternatives.

Una troballa interessant del mateix exercici: l'IllegalArgumentException amb el missatge de l'any invàlid cau a la categoria "bug", quan en realitat era una validació de negoci perfectament esperada. És exactament l'argument a favor de les excepcions pròpies de 06-04: si aquella validació llancés AnyPublicacioInvalidException extends CatalegException, la política la classificaria bé automàticament. Amb excepcions estàndard, la classificació automàtica és impossible.

Solució 2

package com.nexussoftware.bibliotech.servei;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.nexussoftware.bibliotech.domini.BiblioTechException;

/**
 * Embolcalla operacions de BiblioTech aportant registre, mesurament i
 * politica uniforme d'errors.
 *
 * Es un exemple del patro "decorador de serveis" que al modul 11
 * fan els frameworks amb aspectes i anotacions.
 */
public class RegistreOperacions {

    private static final Logger LOG = Logger.getLogger(RegistreOperacions.class.getName());

    /** Estadistiques acumulades d'una operacio (mutable a proposit). */
    private static class Estadistica {
        int execucions;
        int exits;
        int fallades;
        long nanosTotals;

        double mitjanaMs() {
            return (execucions == 0) ? 0.0 : (nanosTotals / 1_000_000.0) / execucions;
        }
    }

    private final Map<String, Estadistica> estadistiques = new HashMap<>();
    private int comptador = 0;

    /**
     * Executa una operacio registrant inici, final, durada i fallades.
     *
     * @param nomOperacio nom logic (PRESTAR, RETORNAR, IMPORTAR...)
     * @param idEmpleat   IDENTIFICADOR de l'empleat, mai el seu nom
     * @param operacio    l'operacio a executar
     */
    public <T> T executar(String nomOperacio, String idEmpleat, Supplier<T> operacio) {
        String idOperacio = seguentId();
        long inici = System.nanoTime();
        boolean exit = false;

        // Forma mandrosa: si FINE esta desactivat, la cadena no es construeix
        LOG.fine(() -> "[" + idOperacio + "] INICI " + nomOperacio
                + " (empleat " + idEmpleat + ")");

        try {
            T resultat = operacio.get();
            exit = true;
            return resultat;

        } catch (BiblioTechException e) {
            // Fallada ESPERADA del domini: WARNING amb l'excepcio com a
            // tercer argument, i es RELLANCA perque decideixi qui cridi.
            LOG.log(Level.WARNING, "[" + idOperacio + "] " + nomOperacio
                    + " rebutjada [" + e.getCodi() + "]", e);
            throw e;

        } catch (RuntimeException e) {
            // Fallada INESPERADA: es un bug. SEVERE amb l'identificador com a
            // incidencia, i s'EMBOLCALLA conservant la causa (06-03).
            LOG.log(Level.SEVERE, "[" + idOperacio + "] Error no controlat a "
                    + nomOperacio, e);
            throw new IllegalStateException(
                    "Error intern a " + nomOperacio + ". Incidencia: " + idOperacio, e);

        } finally {
            // La durada es registra SEMPRE, amb exit i amb fallada (06-05)
            long duracio = System.nanoTime() - inici;
            acumular(nomOperacio, exit, duracio);

            final boolean hiHaHagutExit = exit;
            final double ms = duracio / 1_000_000.0;

            if (hiHaHagutExit) {
                LOG.info(() -> String.format("[%s] FI %s OK (%.2f ms)",
                        idOperacio, nomOperacio, ms));
            } else {
                LOG.fine(() -> String.format("[%s] FI %s FALLIDA (%.2f ms)",
                        idOperacio, nomOperacio, ms));
            }
        }
    }

    /** Variant sense resultat. */
    public void executarSenseResultat(String nomOperacio, String idEmpleat, Runnable operacio) {
        executar(nomOperacio, idEmpleat, () -> {
            operacio.run();
            return null;
        });
    }

    private synchronized void acumular(String nom, boolean exit, long nanos) {
        Estadistica e = estadistiques.computeIfAbsent(nom, n -> new Estadistica());
        e.execucions++;
        e.nanosTotals += nanos;
        if (exit) { e.exits++; } else { e.fallades++; }
    }

    private synchronized String seguentId() {
        comptador++;
        return String.format("OP-%05d", comptador);
    }

    public void bolcarEstadistiques() {
        System.out.println();
        System.out.printf("%-14s %-12s %-9s %-9s %s%n",
                "OPERACIO", "EXECUCIONS", "EXITS", "FALLADES", "MITJANA (ms)");
        System.out.println("-".repeat(62));

        estadistiques.forEach((nom, e) ->
                System.out.printf("%-14s %-12d %-9d %-9d %.2f%n",
                        nom, e.execucions, e.exits, e.fallades, e.mitjanaMs()));
    }

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

    public static void main(String[] args) {
        com.nexussoftware.bibliotech.presentacio.ConfiguracioLog.inicialitzar();

        Cataleg cataleg = new Cataleg();
        cataleg.registrar(new com.nexussoftware.bibliotech.domini.Llibre(
                "LIB-0001", "Java Eficac", "Bloch", 2018, "978-0000000001"));
        cataleg.registrar(new com.nexussoftware.bibliotech.domini.Llibre(
                "LIB-0002", "Patrons de Disseny", "GoF", 1994, "978-0000000002"));

        RegistreOperacions registre = new RegistreOperacions();

        System.out.println("\n----- Operacions -----");

        // 1 i 2: correctes
        registre.executar("CONSULTAR", "EMP-001",
                () -> cataleg.obtenirPerReferencia("LIB-0001").getTitol());
        registre.executar("CONSULTAR", "EMP-002",
                () -> cataleg.obtenirPerReferencia("LIB-0002").getTitol());

        // 3: fallada de domini esperada, es rellanca tal qual
        try {
            registre.executar("CONSULTAR", "EMP-001",
                    () -> cataleg.obtenirPerReferencia("LIB-9999").getTitol());
        } catch (BiblioTechException e) {
            System.out.println("Rebutjada: " + e.getMessage());
        }

        // 4: alta correcta
        registre.executarSenseResultat("ALTA", "EMP-003", () ->
                cataleg.registrar(new com.nexussoftware.bibliotech.domini.Llibre(
                        "LIB-0003", "Refactoritzacio", "Fowler", 1999, "978-0000000003")));

        // 5: alta duplicada, fallada de domini
        try {
            registre.executarSenseResultat("ALTA", "EMP-003", () ->
                    cataleg.registrar(new com.nexussoftware.bibliotech.domini.Llibre(
                            "LIB-0001", "Copia", "X", 2020, "978-0000000009")));
        } catch (BiblioTechException e) {
            System.out.println("Rebutjada: " + e.getMessage());
        }

        // 6: BUG simulat, s'embolcalla amb incidencia
        try {
            registre.executar("INFORME", "EMP-001", () -> {
                String nul = null;
                return nul.length();           // NullPointerException
            });
        } catch (IllegalStateException e) {
            System.out.println("Error intern: " + e.getMessage());
            System.out.println("  causa registrada: " + e.getCause().getClass().getSimpleName());
        }

        registre.bolcarEstadistiques();
    }
}

Sortida (consola, nivell INFO):

----- Operacions -----
[INFO   ] RegistreOperacions [OP-00001] FI CONSULTAR OK (0,08 ms)
[INFO   ] RegistreOperacions [OP-00002] FI CONSULTAR OK (0,01 ms)
[WARNING] RegistreOperacions [OP-00003] CONSULTAR rebutjada [MATERIALNOTROBAT]
          causa: com.nexussoftware.bibliotech.domini.MaterialNoTrobatException: No existeix...
Rebutjada: No existeix cap material amb la referencia 'LIB-9999' (el cataleg te 2 materials)
[INFO   ] Cataleg            Material registrat: LIB-0003 (total 3)
[INFO   ] RegistreOperacions [OP-00004] FI ALTA OK (0,15 ms)
[WARNING] RegistreOperacions [OP-00005] ALTA rebutjada [REFERENCIADUPLICADA]
Rebutjada: Ja existeix un material amb la referencia LIB-0001: 'Java Eficac'
[SEVERE ] RegistreOperacions [OP-00006] Error no controlat a INFORME
Error intern: Error intern a INFORME. Incidencia: OP-00006
  causa registrada: NullPointerException

OPERACIO       EXECUCIONS   EXITS     FALLADES  MITJANA (ms)
--------------------------------------------------------------
CONSULTAR      3            2         1         0.04
ALTA           2            1         1         0.09
INFORME        1            0         1         0.05

Les tres decisions de disseny de l'exercici:

  1. Les fallades de domini es rellancen tal qual; els bugs s'embolcallen. Les primeres ja tenen un missatge apte per a l'usuari i un tipus que la presentació pot distingir; els segons necessiten una incidència i amagar els detalls.
  2. El nivell reflecteix qui ho ha de mirar. WARNING per a un rebuig de negoci —algú podria voler saber quants n'hi ha— i SEVERE per a un bug, que requereix intervenció de desenvolupament.
  3. La durada es mesura al finally, així que es registra també a les fallades. Saber que una operació ha trigat 4 segons abans de fallar és informació valuosa: suggereix un temps d'espera esgotat i no un rebuig immediat.

Solució 3

Els deu problemes:

# Problema Mal
1 catch (Throwable t) al bucle Atrapa OutOfMemoryError i StackOverflowError i continua el bucle, amb la JVM ja inservible
2 t.printStackTrace() com a gestió Bolca un trace cru a la cara de l'usuari: risc de seguretat, inútil per a ell i sense registre
3 new Scanner(System.in) dins del bucle Crea un Scanner nou a cada volta. Amb el sc.close() de prestar() es tanca System.in i totes les lectures posteriors llancen NoSuchElementException
4 sc.close() al finally tancant System.in L'error de 06-06: tanca l'entrada estàndard de tot el procés
5 System.exit(0) dins del try Salta els finally (06-05) i qualsevol neteja pendent
6 switch sense default Una opció invàlida no produeix cap efecte ni missatge: l'usuari creu que no ha funcionat el teclat
7 catch (Exception e) + throw e a prestar Captura massa ample i a sobre rellança després d'imprimir: registre duplicat i el throws Exception contamina la signatura
8 System.out.println(e.getStackTrace()[0]) Filtra la classe i la línia internes a l'usuari. I llança ArrayIndexOutOfBoundsException si el trace està buit (el fast throw de 06-01)
9 catch (Exception e) { } buit a retornar El pitjor error del mòdul: la fallada desapareix sense deixar rastre
10 "Devolucio processada" fora del try S'imprimeix encara que la devolució hagi fallat. L'aplicació menteix a l'usuari

Hi ha un onzè problema estructural: no existeix cap Logger. Tot el diagnòstic és printStackTrace i println, així que no hi ha res per filtrar, arxivar ni analitzar.

Versió corregida:

package com.nexussoftware.bibliotech.presentacio;

import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.GestorPrestecs;

/**
 * Menu principal de BiblioTech, versio final del modul 6.
 *
 * Politica d'errors:
 *   - Un unic Scanner d'instancia sobre System.in, que MAI no es tanca.
 *   - Es captura per TIPUS, del mes especific al mes general.
 *   - Les fallades de domini produeixen missatges per a l'usuari i log FINE.
 *   - Els bugs produeixen un missatge generic amb incidencia i log SEVERE.
 *   - No es captura Throwable: els Error pugen a la frontera del main.
 *   - Zero System.out.println de diagnostic.
 */
public class MenuBiblioTech {

    private static final Logger LOG = Logger.getLogger(MenuBiblioTech.class.getName());

    private static final int DIES_PRESTEC = 15;

    /** UN SOL Scanner, creat una vegada i mai tancat (06-06). */
    private final Scanner scanner = new Scanner(System.in);

    private final GestorPrestecs gestor;
    private int comptadorOperacions = 0;
    private boolean sortir = false;

    public MenuBiblioTech(GestorPrestecs gestor) {
        this.gestor = gestor;
    }

    public void arrencar() {
        LOG.info("Menu de BiblioTech iniciat");

        while (!sortir) {
            mostrarMenu();
            int opcio = llegirOpcio(0, 3);

            // Cada opcio gestiona ELS SEUS PROPIS errors: aqui no hi ha xarxa que
            // capturi de tot, perque una xarxa aixi amagaria bugs.
            switch (opcio) {
                case 1 -> prestar();
                case 2 -> retornar();
                case 3 -> llistar();
                case 0 -> sortir();
                default -> System.out.println("Opcio no reconeguda: " + opcio);
            }
        }
        LOG.info("Menu de BiblioTech finalitzat");
    }

    private void mostrarMenu() {
        System.out.println("""

                ===== BiblioTech - Nexus Software =====
                1. Prestar material
                2. Retornar material
                3. Llistar cataleg
                0. Sortir
                =======================================""");
    }

    /** Reintent indefinit: hi ha una persona davant que pot corregir (06-02). */
    private int llegirOpcio(int min, int max) {
        while (true) {
            System.out.print("Opcio: ");
            String linia = scanner.nextLine().trim();
            try {
                int valor = Integer.parseInt(linia);
                if (valor < min || valor > max) {
                    System.out.printf("Ha d'estar entre %d i %d.%n", min, max);
                    continue;
                }
                return valor;
            } catch (NumberFormatException e) {
                System.out.println("'" + linia + "' no es un numero. Escriviu un digit de "
                        + min + " a " + max + ".");
                LOG.fine(() -> "Entrada no numerica al menu: '" + linia + "'");
            }
        }
    }

    private void prestar() {
        String idOperacio = seguentIdOperacio();

        System.out.print("Referencia del material: ");
        String referencia = scanner.nextLine().trim();
        System.out.print("Identificador de l'empleat: ");
        String idEmpleat = scanner.nextLine().trim();
        int dia = llegirEnterAmbLimit("Dia del prestec (1-365)", 1, 365);
        if (dia == -1) {
            System.out.println("Operacio cancellada.");
            return;
        }

        try {
            gestor.prestar(idOperacio, referencia, idEmpleat, dia);
            // SORTIDA del programa (el que l'usuari ha demanat veure), no log
            System.out.println("Prestat " + referencia + ". Devolucio prevista: dia "
                    + (dia + DIES_PRESTEC));

        } catch (MaterialNoTrobatException e) {
            System.out.println("No existeix el material '" + e.getReferenciaCercada() + "'.");
            LOG.fine(() -> "[" + idOperacio + "] Referencia inexistent: "
                    + e.getReferenciaCercada());

        } catch (MaterialNoDisponibleException e) {
            System.out.println("'" + e.getTitol() + "' esta prestat.");
            System.out.println("Disponible d'aqui a uns " + e.diesEspera(dia) + " dies.");
            LOG.fine(() -> "[" + idOperacio + "] Material no disponible: " + e.getReferencia());

        } catch (LimitPrestecsExceditException e) {
            System.out.printf("Limit de %d prestecs assolit. Retorneu %d material(s).%n",
                    e.getLimit(), e.devolucionsNecessaries());
            LOG.fine(() -> "[" + idOperacio + "] Limit excedit per " + e.getIdEmpleat());

        } catch (BiblioTechException e) {
            System.out.println("No s'ha pogut completar el prestec: " + e.getMessage());
            LOG.log(Level.WARNING, "[" + idOperacio + "] Fallada de domini no prevista ["
                    + e.getCodi() + "]", e);

        } catch (RuntimeException e) {
            // BUG. L'usuari veu una incidencia; el registre, tot el detall.
            System.out.println("Error intern. Incidencia: " + idOperacio);
            LOG.log(Level.SEVERE, "[" + idOperacio + "] Error no controlat en prestar "
                    + referencia + " a " + idEmpleat, e);
        }
        // NO es captura Throwable: un Error ha de pujar a la frontera del main.
    }

    private void retornar() {
        String idOperacio = seguentIdOperacio();

        System.out.print("Referencia del prestec (PR-NNNN): ");
        String referenciaPrestec = scanner.nextLine().trim();
        int dia = llegirEnterAmbLimit("Dia de la devolucio (1-365)", 1, 365);
        if (dia == -1) {
            System.out.println("Operacio cancellada.");
            return;
        }

        try {
            gestor.retornar(idOperacio, referenciaPrestec, dia);
            // DINS del try: nomes s'afirma l'exit si de debo n'hi ha hagut
            System.out.println("Devolucio de " + referenciaPrestec + " processada.");

        } catch (PrestecJaRetornatException e) {
            System.out.println("Aquell prestec ja es va retornar el dia "
                    + e.getDiaDevolucioOriginal() + ".");
            LOG.fine(() -> "[" + idOperacio + "] Devolucio duplicada de "
                    + e.getReferenciaPrestec());

        } catch (MaterialNoTrobatException e) {
            System.out.println("No existeix el prestec '" + e.getReferenciaCercada() + "'.");
            LOG.fine(() -> "[" + idOperacio + "] Prestec inexistent: "
                    + e.getReferenciaCercada());

        } catch (BiblioTechException e) {
            System.out.println("No s'ha pogut completar la devolucio: " + e.getMessage());
            LOG.log(Level.WARNING, "[" + idOperacio + "] Fallada de domini a la devolucio", e);

        } catch (RuntimeException e) {
            System.out.println("Error intern. Incidencia: " + idOperacio);
            LOG.log(Level.SEVERE, "[" + idOperacio + "] Error no controlat en retornar "
                    + referenciaPrestec, e);
        }
    }

    private void llistar() {
        System.out.println("(llistat del cataleg)");
    }

    /**
     * Sortida ORDENADA: es marca el marcador i el bucle acaba sol.
     * Res de System.exit aqui: aixo saltaria els finally i la neteja (06-05).
     */
    private void sortir() {
        sortir = true;
        System.out.println("Fins aviat.");
    }

    private int llegirEnterAmbLimit(String peticio, int min, int max) {
        for (int intent = 1; intent <= 3; intent++) {
            System.out.print(peticio + ": ");
            String linia;
            try {
                linia = scanner.nextLine().trim();
            } catch (NoSuchElementException e) {
                // Passa si System.in es tanca o arriba al final (entrada redirigida)
                LOG.log(Level.WARNING, "Entrada estandard esgotada; es cancella l'operacio", e);
                return -1;
            }
            try {
                int valor = Integer.parseInt(linia);
                if (valor < min || valor > max) {
                    System.out.printf("Fora de rang [%d..%d]. Intent %d de 3.%n",
                            min, max, intent);
                    continue;
                }
                return valor;
            } catch (NumberFormatException e) {
                System.out.printf("'%s' no es un numero. Intent %d de 3.%n", linia, intent);
            }
        }
        return -1;
    }

    private String seguentIdOperacio() {
        comptadorOperacions++;
        return String.format("OP-%05d", comptadorOperacions);
    }
}

Els onze problemes resolts, un a un:

  1. Es captura per tipus, mai Throwable. Els Error pugen a la frontera del main.
  2. Ni un sol printStackTrace: tot va al Logger amb el seu nivell.
  3. Un sol Scanner d'instància, creat al camp.
  4. Mai no es tanca System.in.
  5. System.exit substituït per un marcador i una sortida ordenada del bucle.
  6. switch amb default que informa.
  7. Captures específiques, sense rellançar, i prestar() ja no declara throws.
  8. Zero informació interna en pantalla: identificador d'incidència i res més.
  9. Cap catch buit.
  10. La confirmació d'èxit està dins del try.
  11. Existeix un Logger per classe, amb nivells distingits i missatges mandrosos.

Conclusió

Has tancat el mòdul amb la part que converteix el coneixement tècnic en programari de producció.

En estratègia, tens la regla que governa tota la resta: captura on puguis decidir, no on es produeix, amb les seves tres preguntes —puc fer alguna cosa diferent de propagar?, tinc context per afegir?, soc la frontera?— i la conseqüència que no capturar és una decisió activa i moltes vegades la correcta. Coneixes la política d'errors de cada capa de BiblioTech: el domini valida, llança i mai no registra ni imprimeix, perquè no sap qui hi ha a l'altre costat; el servei aplica regles, tradueix conservant la causa i garanteix estat coherent; la presentació és el filtre que captura tot el que és esperable i decideix quin missatge mostrar.

Saps construir la frontera d'errors del main, amb els seus tres catch ordenats que distingeixen una fallada de domini (missatge net) d'un bug (incidència i silenci sobre els detalls) i d'un Error (registre protegit i sortida immediata) —l'única excepció legítima a la regla de no capturar Error—, amb codis de sortida diferents perquè un script pugui reaccionar. I saps instal·lar el gestor global amb Thread.setDefaultUncaughtExceptionHandler, que impedeix que una excepció escapada d'un fil secundari mati aquell fil en silenci, juntament amb el hook d'aturada de 06-05.

Tens clara la separació entre el que veu l'usuari —missatge en el seu vocabulari, què fer, identificador d'incidència— i el que va al registre —classe, stack trace, camins, context—, amb el motiu de fons: un stack trace en pantalla és un risc de seguretat abans que una molèstia. I l'identificador d'incidència com a peça que uneix tots dos mons sense exposar res.

Distingeixes recuperable d'irrecuperable amb el criteri que decideix: degrada quan el servei reduït continuï sent correcte; avorta quan continuar produiria resultats incorrectes —arrencar sense catàleg és correcte, calcular multes amb una tarifa desconeguda no—. Coneixes les quatre condicions d'un reintent sensat, inclosa la que més s'oblida: la idempotència de l'operació. Saps quan validar prèviament i quan capturar, amb l'advertència de TOCTOU que fa obligatòria l'excepció quan el recurs és compartit. I coneixes les alternatives a llançar: Optional per a l'absència normal de valor i l'objecte resultat per acumular diversos errors alhora en comptes d'avortar al primer.

En logging, tens els set motius pels quals System.out.println no serveix en producció —no es filtra, no es desactiva, no porta data ni nivell ni origen, no s'arxiva i bloqueja—, amb la distinció que evita l'excés contrari: System.out continua sent correcte per a la sortida que l'usuari ha demanat; el que se substitueix és el diagnòstic. Coneixes la taula de nivells i què mereix cadascun, amb l'advertència que quan tot és SEVERE, ningú no mira els errors i que una excepció gestionada correctament poques vegades ho és.

Domines java.util.logging a la pràctica: un Logger static final per classe anomenat amb la classe completa, la jerarquia per paquets que permet activar FINE només a la capa de servei, la forma mandrosa amb Supplier que evita construir missatges que ningú no llegirà, els Handler de consola i de fitxer amb rotació, els Formatter propis, i la configuració per logging.properties que permet pujar el nivell en producció sense recompilar. Saps registrar una excepció correctament —LOG.log(Level.X, "missatge", e), amb el Throwable com a tercer argument— i per què les quatre alternatives són pitjors, inclosa la de registrar i rellançar a cada capa. Saps què no registrar mai, amb el parany del toString() que avui és inofensiu i d'aquí a dos anys inclou un DNI, la tècnica d'emmascarar, i l'advertència formal que en un sistema real això ho revisa el responsable de protecció de dades, no tu. I saps escriure missatges útils amb identificadors en comptes de noms, i correlacionar-los amb un identificador d'operació. Amb el mapa de l'ecosistema real: SLF4J com a façana, Logback i Log4j2 com a implementacions, i la certesa que tot l'après aquí es trasllada tal qual —només canvia l'API—, que és el que veuràs a 11-07.

BiblioTech, en tancar el mòdul 6, és un altre sistema. La seva jerarquia d'excepcions —BiblioTechException abstracta com a arrel, CatalegException i PrestecException com a branques, i cinc fulles amb dades estructurades— anomena cada fallada en el llenguatge del negoci i transporta el que qui captura necessita per decidir. Els seus constructors rebutgen les dades invàlides en comptes de corregir-les amb avisos, així que ja no existeixen préstecs amb referència PR-0000 ni llibres publicats l'any 0. Cataleg ha eliminat el null com a valor de retorn: obtenirPerReferencia llança i cercarPerReferencia retorna Optional. GestorPrestecs valida abans de modificar, marca cada pas amb un marcador i compensa al finally, de manera que una fallada a mitges deixa el material disponible, la quota restituïda i cap préstec fantasma, amb l'excepció arribant intacta. SessioBiblioteca i ExportadorCataleg són AutoCloseable, es tanquen passi el que passi i no perden cap excepció pel camí. MenuBiblioTech captura per tipus, tradueix cada fallada a un missatge comprensible, no mostra mai un detall intern i fa servir un únic Scanner que mai no es tanca. I main té una frontera d'errors amb incidències, codis de sortida i gestor global. No queda un sol System.out.println de diagnòstic a tot el projecte: tot el registre passa per Logger, amb marca de temps, nivell, classe, fil i identificador d'operació, en consola per al que és important i en fitxer rotat per a tot el detall.

De les cinc fragilitats que vas declarar en tancar el mòdul 5, quatre estan resoltes: les dades invàlides ja no avorten el programa, els errors no es comuniquen amb null ni false muts, els avisos no van per System.out.println, i les operacions no deixen l'estat incoherent en fallar a mitges.

Queda la cinquena, i és la més simple d'enunciar: res no es guarda en sortir. El catàleg, els préstecs, les multes, les reserves i l'historial viuen íntegrament en memòria. Tanca BiblioTech i desapareixen tres mòduls de feina. La propera execució comença amb un catàleg buit, com si mai no hagués existit. I hi ha una segona mancança, més discreta: l'aplicació no pot llegir res de l'exterior més enllà del que es teclegi, així que carregar un inventari de mil materials significa teclejar-los un a un.

Al mòdul 7, Entrada/Sortida d'arxius, es resol. Veuràs la lectura i l'escriptura de fitxers amb l'API que aquest mòdul només ha fregat —FileReader, FileWriter i el tancament garantit que ja domines—; els fluxos de bytes i de caràcters i per què són dues jerarquies diferents; BufferedReader i BufferedWriter, i què guanya exactament la memòria intermèdia; la serialització d'objectes, que recuperarà el serialVersionUID que vas declarar a les teves excepcions; l'API NIO.2 amb Path i Files, la forma moderna de treballar amb el sistema de fitxers; i els formats d'intercanvi CSV i Properties, amb els quals BiblioTech per fi carregarà la seva configuració —aquelles constants DIES_PRESTEC, TARIFA_DIARIA, MULTA_MAXIMA i LLINDAR_LLEU que porten sis mòduls escrites a foc al codi— des d'un fitxer extern. En acabar-lo, BiblioTech recordarà. I tot el que has après en aquest mòdul —les excepcions comprovades d'E/S, el try-with-resources, la traducció entre capes, la degradació elegant quan un fitxer no existeix i el registre del que passa— serà exactament l'eina que necessitaràs des de la primera línia.

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