Vas tancar el mòdul 5 amb un inventari honest: BiblioTech gestiona catàleg, préstecs, reserves, avisos i desfer, però és fràgil d'una manera que ja no es pot ignorar. Un Integer.parseInt("vint") tomba l'aplicació. Un false retornat no diu per què ha fallat. Un System.out.println("AVIS: ...") no es pot filtrar, arxivar ni tractar. I una operació que falla a mitges deixa el sistema incoherent.

Aquest mòdul ho resol, i comença pel més bàsic i alhora pel pitjor entès: què és realment una excepció. No és "un error que trenca el programa". És un objecte —amb la seva classe, el seu estat i els seus mètodes— que representa una fallada i que activa un mecanisme de transport molt particular: en comptes de retornar-se a qui crida com un valor, viatja cap enrere per la pila de crides buscant algú que se'n faci càrrec. Entendre aquest viatge és entendre tota la resta.

En aquesta lliçó no escriuràs ni un try ni un catch. Això és la lliçó següent. Aquí es tracta de llegir i comprendre la fallada: per què els codis de retorn són una solució pitjor, què passa exactament quan ningú no captura una excepció, com es llegeix un stack trace de dalt a baix —inclosa la secció Caused by:, que és on sol ser la veritat—, com està organitzada la jerarquia Throwable i què significa cada branca, i quina és la diferència entre excepcions comprovades i no comprovades, que és la decisió de disseny més discutida del llenguatge. Quan acabis, un bolcat d'excepció deixarà de ser un mur de text intimidant i passarà a ser el que realment és: un informe detallat que et diu exactament què ha fallat i on.

Contingut

  1. El punt de partida: el false mut de BiblioTech
  2. Què és una excepció
  3. Per què els codis de retorn són pitjors
  4. Què passa quan ningú no captura: propagació per la pila
  5. Anatomia d'un stack trace
  6. Caused by:: la cadena de causes
  7. La jerarquia de Throwable
  8. Error: allò que no has d'intentar gestionar
  9. RuntimeException: errors de programació
  10. Les excepcions comprovades
  11. Comprovades enfront de no comprovades: el criteri de disseny
  12. El debat real sobre les excepcions comprovades
  13. Els missatges de NullPointerException de Java 14+
  14. Què no has de fer mai
  15. Errors Comuns i Consells
  16. Exercicis

  1. El punt de partida: el false mut de BiblioTech

Aquest és un mètode real del BiblioTech que vas construir al mòdul 5. Fixa-t'hi bé, perquè conté el problema que dona sentit a tot el mòdul:

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

/** Versio del modul 5: comunica les fallades amb booleans i amb println. */
public class Cataleg {

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

    public boolean registrar(Material material) {
        if (material == null) {
            System.out.println("AVIS: material nul");
            return false;                                   // (1)
        }
        if (indexPerReferencia.containsKey(material.getReferencia())) {
            System.out.println("AVIS: referencia duplicada");
            return false;                                   // (2)
        }
        if (material instanceof Llibre llibre && !isbnRegistrats.add(llibre.getIsbn())) {
            System.out.println("AVIS: ISBN duplicat");
            return false;                                   // (3)
        }
        materials.add(material);
        indexPerReferencia.put(material.getReferencia(), material);
        return true;
    }

    public Material cercarPerReferencia(String referencia) {
        return indexPerReferencia.get(referencia);          // retorna null si no existeix
    }
}

El mètode funciona. Però mira què li arriba a qui el crida:

boolean ok = cataleg.registrar(material);
if (!ok) {
    // I ara que? Per que ha fallat?
    // Era nul? Referencia duplicada? ISBN duplicat?
    // El boolea no ho diu. Els tres casos son el mateix false.
}

Aquest és el false mut: un valor que informa que alguna cosa ha anat malament, però que esborra tota la informació sobre què ha anat malament. Tres causes radicalment diferents —una fallada de programació (null), un conflicte de dades (referència repetida) i una violació d'una regla de negoci (ISBN ja catalogat)— col·lapsen en el mateix bit. I qui crida no pot reaccionar de manera diferent a cadascuna, perquè no sap quina ha passat.

Encara pitjor: el motiu real s'ha imprès per consola, on el programa no el pot llegir. Un System.out.println no és un canal de comunicació entre mètodes; és un abocador de text. Si registrar es crida des d'un servei web, des d'un procés per lots o des d'un test automatitzat, ningú no veu aquell avís.

I cercarPerReferencia encara és més perillós, perquè retorna null en el cas normal de "no existeix". El null viatja tan tranquil pel programa fins que algú l'utilitza:

Material m = cataleg.cercarPerReferencia("LIB-999");    // no existeix: retorna null
System.out.println(m.getTitol());                       // NullPointerException, molt lluny de l'origen

L'explosió passa en un lloc diferent d'on hi havia el problema. L'error real va ser a la línia 1 (cercar una cosa que no existeix); el símptoma apareix a la línia 2. Aquest desplaçament entre causa i símptoma és una de les fonts més grans de temps perdut depurant.

Les excepcions resolen exactament això: permeten assenyalar una fallada amb nom, amb dades i en el moment exacte en què es detecta.

  1. Què és una excepció

Una excepció és un objecte Java, instància d'una classe que descendeix de java.lang.Throwable, que representa una condició anòmala durant l'execució del programa.

Les tres paraules importants d'aquesta definició:

  • Objecte: no és una paraula clau màgica ni un codi numèric. És un objecte normal, amb el seu tipus, els seus camps i els seus mètodes. Es pot crear amb new, guardar en una variable, passar com a paràmetre i fins i tot emmagatzemar en una llista. El que el fa especial és com es transporta.
  • Representa una condició anòmala: un fitxer que no hi és, un número mal escrit, un índex fora de rang, una regla de negoci violada. Alguna cosa que impedeix al mètode complir el seu contracte.
  • Durant l'execució: no és un error de compilació. El codi compila perfectament; la fallada apareix quan s'executa amb unes dades determinades.

Mira qualsevol excepció com el que és —un objecte amb estat:

public class QueEsUnaExcepcio {
    public static void main(String[] args) {
        // Una excepcio es crea amb new, com qualsevol altre objecte.
        // Crear-la NO la llanca: aqui simplement esta en una variable.
        IllegalArgumentException fallada =
                new IllegalArgumentException("La tarifa diaria no pot ser negativa: -0.25");

        System.out.println("Classe  : " + fallada.getClass().getName());
        System.out.println("Missatge: " + fallada.getMessage());
        System.out.println("toString: " + fallada);
        System.out.println("Marcs   : " + fallada.getStackTrace().length);
        System.out.println("Causa   : " + fallada.getCause());

        System.out.println("El programa segueix viu: crear una excepcio no la llanca.");
    }
}

Sortida:

Classe  : java.lang.IllegalArgumentException
Missatge: La tarifa diaria no pot ser negativa: -0.25
toString: java.lang.IllegalArgumentException: La tarifa diaria no pot ser negativa: -0.25
Marcs   : 1
Causa   : null
El programa segueix viu: crear una excepcio no la llanca.

Tres observacions que convé gravar des del principi:

  1. Crear una excepció no la llança. L'objecte existeix, però el flux del programa no s'altera. Llançar-la requereix la paraula throw, que veuràs a 06-03.
  2. L'objecte ja conté la pila de crides en el moment de la seva construcció (getStackTrace() retorna un array de marcs). Aquesta captura passa al constructor, no en llançar-la. És la part més costosa de crear una excepció, i tornarà a aparèixer a 06-02 en parlar de rendiment.
  3. El missatge és text lliure, i la seva qualitat depèn enterament de qui l'escriu. "error" i "La tarifa diaria no pot ser negativa: -0.25" costen el mateix; només un dels dos serveix per a alguna cosa.

Quan una excepció es llança, passen dues coses simultànies:

  • L'execució del mètode s'interromp immediatament en aquell punt. Les línies següents del mètode no s'executen.
  • L'objecte excepció comença a propagar-se cap enrere per la pila de crides, buscant un gestor. Aquest viatge és l'apartat 4.

  1. Per què els codis de retorn són pitjors

Abans de les excepcions —i encara avui en llenguatges com C o Go— les fallades es comuniquen amb el valor de retorn: -1, null, false, un codi numèric. Java permet fer-ho, i de vegades fins i tot és el correcte. Però com a mecanisme general té quatre defectes greus.

Problema Codi de retorn Excepció
Es pot ignorar Sí: cataleg.registrar(m); compila i descarta el resultat en silenci No: si no es gestiona, el programa s'atura sorollosament
Transporta informació Un false no diu per què; un -1 tampoc Classe + missatge + dades pròpies + pila completa
Ocupa el valor de retorn Si el mètode ja retorna un Material, cal inventar-se un valor "impossible" (null) El valor de retorn queda lliure per al seu propòsit real
Contamina qui crida Cada crida necessita el seu if de comprovació, barrejat amb la lògica El camí normal queda net; la gestió se separa

El primer defecte és el decisiu. Compara:

// Amb codi de retorn: ignorar la fallada es TRIVIAL i a mes invisible
cataleg.registrar(materialDuplicat);        // retorna false... i a ningu no li importa
processarComSiEstiguesRegistrat();          // el programa segueix amb dades incorrectes

// Amb excepcio: ignorar la fallada es IMPOSSIBLE en silenci
cataleg.registrar(materialDuplicat);        // llanca ReferenciaDuplicadaException
processarComSiEstiguesRegistrat();          // aquesta linia NO s'executa

Amb el booleà, el programa continua amb una premissa falsa: creu que el material està registrat i no ho està. Els errors que es propaguen silenciosament contaminant l'estat són molt més cars de diagnosticar que els que aturen el programa, perquè el símptoma apareix minuts o hores després, en un altre lloc, sense relació aparent amb la causa.

El quart defecte, la contaminació de qui crida, es veu millor amb un flux complet. Així queda un préstec amb codis de retorn:

// ESTIL CODIS DE RETORN: la logica real esta enterrada entre comprovacions
public boolean prestar(String referencia, String idEmpleat, int dia) {
    Material m = cataleg.cercarPerReferencia(referencia);
    if (m == null) { return false; }                 // no existeix... o potser si i ha fallat una altra cosa
    if (!m.estaDisponible()) { return false; }       // no disponible
    Empleat e = registreEmpleats.cercar(idEmpleat);
    if (e == null) { return false; }                 // empleat desconegut
    if (!e.potPrendrePrestat()) { return false; }    // limit excedit
    boolean ok = registre.anotar(m, e, dia);
    if (!ok) { return false; }                       // per que?
    return true;
}

Cinc if de guarda, cinc return false indistingibles, i qui cridi prestar rebrà un sol booleà amb cinc significats possibles. I encara més, cadascun d'aquests false que retorna prestar acabarà convertit en un altre false a la capa superior, perdent encara més informació a cada salt.

Al final del mòdul, aquest mateix mètode llançarà MaterialNoTrobatException, MaterialNoDisponibleException, EmpleatDesconegutException o LimitPrestecsExceditException, cadascuna amb les dades concretes de la fallada, i la capa de presentació podrà decidir quin missatge mostrar en cada cas.

Dit això, els codis de retorn no sempre són dolents. Map.get retornant null i Set.add retornant false són dissenys perfectament raonables, perquè en aquests casos "no hi és" i "ja hi era" són resultats normals, no fallades. La regla informal és: si la condició forma part del funcionament esperat i qui crida la comprovarà sempre, un valor de retorn està bé; si és una fallada que impedeix complir el contracte del mètode, és una excepció. A 06-07 tornaràs sobre aquest criteri amb una taula completa.

  1. Què passa quan ningú no captura: propagació per la pila

Aquí hi ha el mecanisme central del mòdul. Recupera la pila de crides de 05-08: cada vegada que un mètode en crida un altre, la JVM apila un marc (stack frame) amb les variables locals i el punt de retorn; quan el mètode acaba, el seu marc es desapila.

Quan es llança una excepció, aquest desapilat passa de cop i sense executar la resta dels mètodes. És el que s'anomena desenrotllament de la pila (stack unwinding).

Observa un cas concret de BiblioTech:

package com.nexussoftware.bibliotech.presentacio;

public class DemoPropagacio {

    public static void main(String[] args) {
        System.out.println("1. main comenca");
        processarSolicitud("dotze");               // l'usuari va escriure "dotze" en comptes de 12
        System.out.println("2. main acaba");       // MAI NO S'EXECUTA
    }

    static void processarSolicitud(String entrada) {
        System.out.println("3. processarSolicitud comenca");
        int dies = calcularDies(entrada);
        System.out.println("4. dies = " + dies);   // MAI NO S'EXECUTA
    }

    static int calcularDies(String text) {
        System.out.println("5. calcularDies comenca");
        return Integer.parseInt(text);             // AQUI es llanca NumberFormatException
    }
}

Sortida real:

1. main comenca
3. processarSolicitud comenca
5. calcularDies comenca
Exception in thread "main" java.lang.NumberFormatException: For input string: "dotze"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:665)
	at java.base/java.lang.Integer.parseInt(Integer.java:781)
	at com.nexussoftware.bibliotech.presentacio.DemoPropagacio.calcularDies(DemoPropagacio.java:17)
	at com.nexussoftware.bibliotech.presentacio.DemoPropagacio.processarSolicitud(DemoPropagacio.java:12)
	at com.nexussoftware.bibliotech.presentacio.DemoPropagacio.main(DemoPropagacio.java:6)

Els missatges 2 i 4 no apareixen. No és que se'ls hagin saltat: és que els seus mètodes van ser abandonats a mitja execució. Cap línia posterior al punt de llançament no va arribar a executar-se en cap dels mètodes de la cadena.

Aquest és el viatge complet:

flowchart TB
    subgraph abans["Pila just abans de la fallada"]
        direction TB
        A3["parseInt(text) ← CIM"]
        A2["calcularDies('dotze')"]
        A1["processarSolicitud('dotze')"]
        A0["main(args) ← FONS"]
        A3 --> A2 --> A1 --> A0
    end
    abans -->|"es llanca NumberFormatException"| B1
    subgraph desenrotllament["Desenrotllament: busca gestor i no el troba"]
        direction TB
        B1["parseInt: sense catch, es descarta el marc"]
        B2["calcularDies: sense catch, es descarta el marc"]
        B3["processarSolicitud: sense catch, es descarta el marc"]
        B4["main: sense catch, es descarta el marc"]
        B1 --> B2 --> B3 --> B4
    end
    B4 --> C["L'excepcio arriba a la JVM:<br/>gestor per defecte del fil"]
    C --> D["Imprimeix el stack trace a System.err"]
    D --> E["El fil 'main' ACABA<br/>codi de sortida diferent de zero"]

Els passos, en detall:

  1. Integer.parseInt detecta que "dotze" no és un número i llança un objecte NumberFormatException.
  2. La JVM busca al mètode actual un gestor (catch) capaç de tractar aquell tipus. No n'hi ha. Descarta el marc de parseInt.
  3. Puja a qui l'ha cridat, calcularDies. Tampoc no hi ha catch. Descarta el seu marc. El return no es completa mai.
  4. Puja a processarSolicitud. Tampoc. Descarta el seu marc. La línia del println("4. ...") queda sense executar.
  5. Puja a main. Tampoc. Descarta el seu marc.
  6. Ja no hi ha més marcs: l'excepció arriba al gestor d'excepcions no capturades per defecte del fil, que imprimeix Exception in thread "main" ... seguit del stack trace a System.err (no a System.out).
  7. El fil main acaba. Si no queden altres fils no daemon vius, la JVM s'apaga amb un codi de sortida diferent de zero (típicament 1), que és el que un script o un sistema d'integració contínua interpreta com "aquest procés ha fallat".

Tres precisions importants que gairebé ningú no explica:

  • Acaba el fil, no necessàriament la JVM. En un programa d'un sol fil, coincideixen. Però si una excepció escapa d'un fil secundari (mòdul 8), aquell fil mor i la resta del programa segueix funcionant, sovint sense que ningú se n'assabenti. És una font clàssica de fallades silencioses.
  • El stack trace s'imprimeix a System.err. Si redirigeixes la sortida estàndard a un fitxer però no la d'error, o a l'inrevés, veuràs la meitat de la història. I com que System.out i System.err es buiden de manera independent, és habitual que a la consola apareguin barrejats i desordenats, cosa que despista molt en depurar.
  • Es pot substituir aquest gestor per defecte. Thread.setDefaultUncaughtExceptionHandler permet decidir què passa amb les excepcions que escapen de tot. És una de les peces de la "frontera d'errors" que muntaràs a 06-07.

  1. Anatomia d'un stack trace

A 02-05 vas aprendre a llegir un stack trace com a eina de depuració. Ara toca la lectura completa i precisa, perquè el stack trace és el document més valuós que tens quan alguna cosa falla en producció i no la pots reproduir.

Torna al bolcat de l'apartat anterior, ara anotat:

Exception in thread "main" java.lang.NumberFormatException: For input string: "dotze"
└──── (A) ────┘ └─ (B) ─┘  └──────── (C) ───────────┘  └────────── (D) ───────────┘
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:665)
	at java.base/java.lang.Integer.parseInt(Integer.java:781)
	at com.nexussoftware.bibliotech.presentacio.DemoPropagacio.calcularDies(DemoPropagacio.java:17)
	at com.nexussoftware.bibliotech.presentacio.DemoPropagacio.processarSolicitud(DemoPropagacio.java:12)
	at com.nexussoftware.bibliotech.presentacio.DemoPropagacio.main(DemoPropagacio.java:6)
	└─ (E) ─┘└─────────── (F) ─────────────┘└──── (G) ────┘└──────── (H) ───────┘
Marca Element Què et diu
(A) Exception in thread Text fix del gestor per defecte: ningú no ha capturat això
(B) "main" Nom del fil que ha mort. Clau en programes multifil
(C) java.lang.NumberFormatException Classe de l'excepció: el què ha fallat
(D) For input string: "dotze" Missatge: el detall concret, amb la dada culpable
(E) at Cada línia at és un marc de la pila
(F) com...DemoPropagacio Classe amb el seu paquet complet
(G) .calcularDies Mètode on era l'execució
(H) (DemoPropagacio.java:17) Fitxer i número de línia exactes

I ara la regla d'or per llegir-lo:

La primera línia at és on ha passat la fallada. L'última és on ha començat tot. El teu codi sol ser al mig.

L'ordre és del cim al fons de la pila, és a dir, del punt més profund cap enrere. Per això main sempre apareix al final: és el fons de la pila.

Estratègia pràctica de lectura, en aquest ordre:

  1. Llegeix la classe i el missatge (línia 1). Moltes vegades amb això n'hi ha prou: NumberFormatException: For input string: "dotze" és un diagnòstic complet.
  2. Baixa fins a la primera línia que contingui el teu paquet (com.nexussoftware.bibliotech). Les línies de java.base/..., de Spring, d'Hibernate o de qualsevol llibreria solen ser només el camí; l'error gairebé sempre és en com tu les vas cridar. A l'exemple, DemoPropagacio.calcularDies(DemoPropagacio.java:17) és la primera línia "teva": aquí hi ha el problema.
  3. Continua baixant per reconstruir el camí: qui ha cridat qui i amb quines dades. Això et diu com s'ha arribat a aquella situació, que és el que sol faltar.
  4. Busca Caused by:, si n'hi ha. És l'apartat següent, i és on hi ha la veritat a les aplicacions amb frameworks.

Un detall que confon molta gent: a l'exemple, Integer.parseInt apareix dues vegades amb línies diferents (665 i 781). No és un error del bolcat: parseInt(String) és una sobrecàrrega que crida internament parseInt(String, int radix). Cada marc és una invocació diferent, encara que el nom del mètode es repeteixi.

Una altra observació: java.base/ davant del nom de la classe és el mòdul de la plataforma (JPMS, Java 9+, tema de 10-06). Indica que aquella classe ve del JDK, no del teu codi ni d'una llibreria. És un filtre visual molt útil: tot el que comença per java.base/ és de la biblioteca estàndard.

Finalment, dos casos que sorprenen:

  • Stack traces truncats amb ... 23 more. Java no repeteix els marcs comuns entre una excepció i la seva causa. ... 23 more significa "els 23 marcs següents són idèntics als del bloc anterior". No és informació perduda.
  • Traces buits o sense línies. Amb l'optimització del JIT, certes excepcions molt repetides (com les NullPointerException llançades en un bucle calent) poden acabar llançant-se sense stack trace per una optimització anomenada fast throw. Si veus una excepció sense trace, arrenca-la amb -XX:-OmitStackTraceInFastThrow i tornarà a aparèixer complet.

  1. Caused by:: la cadena de causes

En una aplicació real amb capes —i BiblioTech ho serà al final d'aquest mòdul—, una excepció de baix nivell s'embolcalla en una altra de més alt nivell abans de continuar pujant. El resultat és una cadena de causes, i el stack trace les mostra totes.

Un exemple amb l'estructura típica de tres capes:

Exception in thread "main" com.nexussoftware.bibliotech.servei.PrestecFallitException: No s'ha pogut registrar el prestec del material LIB-0001 per a l'empleat EMP-004
	at com.nexussoftware.bibliotech.servei.GestorPrestecs.prestar(GestorPrestecs.java:88)
	at com.nexussoftware.bibliotech.presentacio.MenuBiblioTech.opcioPrestar(MenuBiblioTech.java:142)
	at com.nexussoftware.bibliotech.presentacio.BiblioTechApp.main(BiblioTechApp.java:31)
Caused by: com.nexussoftware.bibliotech.domini.MaterialNoDisponibleException: El material LIB-0001 esta prestat des del dia 12
	at com.nexussoftware.bibliotech.domini.Material.prestar(Material.java:64)
	at com.nexussoftware.bibliotech.servei.GestorPrestecs.prestar(GestorPrestecs.java:84)
	... 2 more
Caused by: java.lang.IllegalStateException: El comptador de prestecs acumulats es negatiu: -1
	at com.nexussoftware.bibliotech.domini.Empleat.registrarPrestec(Empleat.java:47)
	at com.nexussoftware.bibliotech.domini.Material.prestar(Material.java:61)
	... 3 more

Com es llegeix això:

Bloc Què és Utilitat
El primer L'excepció de més alt nivell, la que ha arribat a la JVM Diu quina operació de negoci ha fallat
Cada Caused by: L'excepció que va provocar l'anterior Baixa un nivell d'abstracció
L'últim Caused by: La causa arrel És on hi ha el problema real

Regla pràctica: llegeix la primera línia per saber quina operació ha fallat i baixa a l'últim Caused by: per saber per què. El mig és el camí entre totes dues.

A l'exemple, l'operació que ha fallat és "registrar un préstec" (bloc 1), la raó immediata és que el material no estava disponible (bloc 2), però la causa arrel és un comptador de préstecs que s'ha tornat negatiu (bloc 3). Arreglar el nivell superior no serviria de res: el problema és a Empleat.registrarPrestec.

I aquí una avançada important de 06-03: aquesta cadena només existeix si qui embolcalla l'excepció en conserva la causa. Si a GestorPrestecs.prestar algú hagués escrit:

// MALAMENT: perd tota la informacio del que realment ha passat
throw new PrestecFallitException("No s'ha pogut registrar el prestec");

...el stack trace s'hauria quedat al primer bloc, i els dos Caused by: —els que contenen la informació útil— haurien desaparegut per sempre. Aquest error, que a 06-03 anomenaràs "perdre la causa", és responsable d'una quantitat enorme d'hores malgastades en producció.

  1. La jerarquia de Throwable

Tot el que es pot llançar en Java descendeix d'una única classe: java.lang.Throwable. La seva descendència s'organitza així:

flowchart TB
    T["Throwable<br/>(arrel: tot allo llancable)"]
    T --> E["Error<br/>fallades greus de la JVM<br/>NO comprovades"]
    T --> X["Exception<br/>condicions anomales<br/>COMPROVADES"]
    X --> R["RuntimeException<br/>errors de programacio<br/>NO comprovades"]

    E --> E1["StackOverflowError"]
    E --> E2["OutOfMemoryError"]
    E --> E3["NoClassDefFoundError"]

    X --> X1["IOException"]
    X --> X2["SQLException"]
    X --> X3["ClassNotFoundException"]
    X1 --> X4["FileNotFoundException"]

    R --> R1["NullPointerException"]
    R --> R2["IllegalArgumentException"]
    R --> R3["IllegalStateException"]
    R --> R4["IndexOutOfBoundsException"]
    R --> R5["ClassCastException"]
    R --> R6["ArithmeticException"]
    R2 --> R7["NumberFormatException"]
    R4 --> R8["ArrayIndexOutOfBoundsException"]
    R4 --> R9["StringIndexOutOfBoundsException"]

Aquesta jerarquia no és decorativa. Té tres conseqüències pràctiques que governen tota la resta:

  1. Només es poden llançar i capturar objectes que descendeixin de Throwable. No pots llançar un String ni un int.
  2. La branca determina si el compilador t'obliga a alguna cosa. Exception (excloent-hi RuntimeException) és comprovada: el compilador exigeix capturar-la o declarar-la. Error i RuntimeException són no comprovades: el compilador no diu res.
  3. Capturar una superclasse captura tots els seus descendents. catch (Exception e) captura també NumberFormatException, perquè en descendeix. Això és el que dona sentit —i perill— a les captures amples, i el que fa obligatori l'ordre dels catch que veuràs a 06-02.

El que Throwable aporta a tots els seus descendents:

Mètode Què retorna
getMessage() El missatge de detall que es va passar al constructor
getLocalizedMessage() Igual, però pensat per sobreescriure's amb traducció
toString() nom.complet.de.la.Classe: missatge
printStackTrace() Imprimeix el trace complet a System.err
getStackTrace() L'array de StackTraceElement amb els marcs
getCause() L'excepció que la va provocar, o null
getSuppressed() Excepcions suprimides (06-06)

Fixa't que Throwabledos fills directes amb destins oposats: Error, que no has de gestionar, i Exception, que sí. Aquesta separació és el motiu pel qual mai no es captura Throwable: fer-ho posa al mateix sac coses que es tracten de manera completament diferent.

  1. Error: allò que no has d'intentar gestionar

La branca Error representa fallades de la màquina virtual o de l'entorn, no de la lògica del teu programa. Són situacions de les quals, en general, una aplicació no es pot recuperar de manera sensata.

Error Causa típica
StackOverflowError Pila de crides esgotada: recursió sense cas base o massa profunda (ho vas veure a 05-08)
OutOfMemoryError El monticle s'ha esgotat: fuita de memòria, col·lecció que creix sense límit, o -Xmx insuficient
NoClassDefFoundError Una classe que hi era en compilar no apareix al classpath en executar
ExceptionInInitializerError Una excepció ha escapat d'un inicialitzador estàtic (static { ... })
AssertionError Una asserció assert ha fallat (o la llança un framework de tests)

El StackOverflowError és el més fàcil de provocar, i ja el coneixes del mòdul 5:

public class RecursioSenseFre {
    // Sense cas base: cada crida apila un marc mes... fins a esgotar la pila
    static int comptarPrestecs(int n) {
        return 1 + comptarPrestecs(n - 1);
    }

    public static void main(String[] args) {
        comptarPrestecs(10);   // StackOverflowError despres de ~10.000-50.000 marcs
    }
}

El seu stack trace és recognoscible a l'instant: milers de línies idèntiques repetides.

Exception in thread "main" java.lang.StackOverflowError
	at RecursioSenseFre.comptarPrestecs(RecursioSenseFre.java:4)
	at RecursioSenseFre.comptarPrestecs(RecursioSenseFre.java:4)
	at RecursioSenseFre.comptarPrestecs(RecursioSenseFre.java:4)
	... (milers de linies iguals)

Quan vegis això, no busquis l'error a la línia que es repeteix: busca'l a la condició d'aturada que falta o que no es compleix mai.

La regla amb els Error és taxativa: no els capturis. Si el teu programa es queda sense memòria, capturar l'OutOfMemoryError no arregla res —el més probable és que el següent new torni a fallar, i a més el mateix gestor pot necessitar memòria per executar-se—. La resposta correcta és deixar que el procés mori, que un supervisor el reiniciï, i analitzar-ne la causa: un bolcat de monticle, una fuita, un límit mal dimensionat. Això és matèria de 10-07.

L'única excepció raonable a aquesta regla és un servidor d'aplicacions o un framework que captura Throwable al seu bucle principal per registrar-lo abans de morir, mai per continuar com si res. Tu, escrivint lògica de negoci, no tens aquest cas.

  1. RuntimeException: errors de programació

RuntimeException i els seus descendents són excepcions no comprovades: el compilador no obliga a capturar-les ni a declarar-les. El criteri de disseny darrere d'aquesta decisió és clar i mereix recordar-se:

Una RuntimeException assenyala normalment un error de programació: alguna cosa que no hauria d'haver passat si el codi estigués ben escrit. La solució no és capturar-la, és corregir el codi.

Aquestes són les que veuràs més, amb la seva causa típica i el seu remei real:

Excepció Causa típica Com s'arregla de debò
NullPointerException Fer servir un membre d'una referència null Comprovar abans, o no permetre el null d'origen (Objects.requireNonNull, 06-03)
ArrayIndexOutOfBoundsException array[i] amb i < 0 o i >= length Revisar el límit del bucle (< enfront de <=)
StringIndexOutOfBoundsException charAt/substring fora de rang Comprovar la longitud abans
IndexOutOfBoundsException list.get(i) fora de rang Ídem, amb size()
NumberFormatException Integer.parseInt("dotze") Validar l'entrada o capturar a la frontera (06-02)
ArithmeticException Divisió entera per zero: 5 / 0 Comprovar el divisor. Amb double, 5.0/0 dona Infinity, no excepció
ClassCastException Convertir a un tipus incompatible Fer servir instanceof amb patró, o genèrics (10-01)
IllegalArgumentException Un mètode rep un argument invàlid Validar en qui crida; la llança el mètode que la detecta
IllegalStateException Cridar un mètode en un moment inadequat Revisar el cicle de vida de l'objecte
UnsupportedOperationException Modificar una col·lecció immutable (List.of, Arrays.asList) Copiar a una llista mutable
ConcurrentModificationException Modificar una col·lecció mentre s'itera (05-04) Fer servir Iterator.remove o removeIf
NoSuchElementException next() sense més elements; remove() sobre cua buida (05-07) Comprovar hasNext(), o fer servir poll()

Un programa curt que en provoca diverses a propòsit, per reconèixer-les pel seu missatge:

package com.nexussoftware.bibliotech.presentacio;

import java.util.List;

/** Provoca excepcions tipiques d'una en una. Executa amb el numero de cas. */
public class MostraDeFallades {

    public static void main(String[] args) {
        int cas = args.length > 0 ? Integer.parseInt(args[0]) : 1;

        switch (cas) {
            case 1 -> {
                String titol = null;
                System.out.println(titol.length());
                // NullPointerException: Cannot invoke "String.length()" because "titol" is null
            }
            case 2 -> {
                String[] isbns = { "978-0000000001", "978-0000000002" };
                System.out.println(isbns[2]);
                // ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
            }
            case 3 -> {
                System.out.println(Integer.parseInt("quinze"));
                // NumberFormatException: For input string: "quinze"
            }
            case 4 -> {
                int diesTotals = 30, materials = 0;
                System.out.println(diesTotals / materials);
                // ArithmeticException: / by zero
            }
            case 5 -> {
                Object o = "Java Eficac";
                Integer n = (Integer) o;
                System.out.println(n);
                // ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
            }
            case 6 -> {
                List<String> fixa = List.of("Java Eficac", "Refactoritzacio");
                fixa.add("Patrons de Disseny");
                // UnsupportedOperationException (sense missatge)
            }
            default -> System.out.println("Cas desconegut: " + cas);
        }
    }
}

Presta atenció a un detall del cas 4: la divisió entera per zero llança excepció, però la divisió en coma flotant no. 30 / 0 explota; 30.0 / 0.0 retorna NaN i 30.0 / 0 retorna Infinity, en silenci. És una asimetria de l'especificació IEEE 754 que sorprèn, i una font real de càlculs de multes absurds: si TARIFA_DIARIA es dividís accidentalment per zero, BiblioTech no fallaria, simplement començaria a mostrar Infinity € als rebuts.

  1. Les excepcions comprovades

Una excepció comprovada (checked) és qualsevol descendent d'Exception que no descendeixi de RuntimeException. Sobre aquestes el compilador aplica l'anomenada regla de capturar o declarar (catch or specify): si un mètode les pot llançar, qui el crida està obligat a fer una de dues coses.

Comprova-ho tu mateix. Aquest codi no compila:

import java.io.FileReader;

public class NoCompila {
    public static void main(String[] args) {
        FileReader lector = new FileReader("cataleg.txt");
        // error: unreported exception java.io.FileNotFoundException;
        //        must be caught or declared to be thrown
    }
}

El compilador es planta. Les dues sortides legals seran les de 06-02 i 06-03: capturar-la amb try-catch, o declarar-la amb throws a la signatura perquè el problema sigui de qui crida.

Aquestes són les comprovades més freqüents:

Excepció comprovada Quan apareix Mòdul on es treballa
IOException Qualsevol operació d'entrada/sortida que pot fallar Mòdul 7
FileNotFoundException El fitxer no existeix o no es pot obrir (filla d'IOException) Mòdul 7
InterruptedException Un fil adormit o esperant és interromput Mòdul 8
SQLException Fallada de base de dades Mòdul 11
ClassNotFoundException Class.forName no troba la classe Mòdul 10 (reflexió)
CloneNotSupportedException clone() sobre una classe que no implementa Cloneable Mòdul 3

La intenció original del disseny és aquesta: una excepció comprovada representa una condició anòmala però previsible i potencialment recuperable, aliena al control del programador. Que un fitxer no existeixi no és un error del teu codi: és un fet del món. Per això el llenguatge t'obliga a decidir explícitament què fer-hi, en comptes de deixar que el programa caigui.

  1. Comprovades enfront de no comprovades: el criteri de disseny

Aquesta taula resumeix la diferència completa, i convé tornar-hi quan a 06-04 hagis de triar de quina classe hereten les teves pròpies excepcions:

Aspecte Comprovades No comprovades
Classe base Exception (no RuntimeException) RuntimeException i Error
Obliga el compilador? Sí: capturar o declarar No
Apareixen a throws? Obligatori si es propaguen Opcional (només documenta)
Significat previst Condició externa i recuperable Error de programació o fallada greu
Reacció esperada Gestionar-la: reintentar, degradar, avisar Corregir el codi
Exemples IOException, SQLException, InterruptedException NullPointerException, IllegalArgumentException
Cas de BiblioTech "El fitxer de catàleg no es pot llegir" "Algú ha passat un Material nul a registrar"
Cost Contamina les signatures de tota la cadena de crides Pot passar desapercebuda fins que explota

La pregunta que t'ajudarà a decidir a la pràctica:

Pot qui crida fer alguna cosa sensata i diferent de "morir" quan això passi?

  • → una comprovada és defensable: demanar el fitxer un altre cop, fer servir valors per defecte, reintentar.
  • No, això no hauria de passar mai si el codi està bé → no comprovada.

Aplicat a BiblioTech, que és la decisió que prendràs formalment a 06-04:

Situació Tipus triat Raó
El material buscat no existeix al catàleg No comprovada Qui cerca hauria d'haver comprovat abans; i obligar a un try a cada cerca seria insuportable
L'empleat ha assolit el límit de 3 préstecs No comprovada És una regla de negoci coneguda i consultable amb potPrendrePrestat()
El fitxer de catàleg no es pot llegir en arrencar Comprovada És un fet extern, i a dalt es pot decidir arrencar amb catàleg buit
Es registra un material null No comprovada (NullPointerException) Error pur de programació

  1. El debat real sobre les excepcions comprovades

Java és pràcticament l'únic llenguatge majoritari amb excepcions comprovades. C#, Kotlin, Scala, Python, JavaScript i Go les van descartar deliberadament. Convé que coneguis el debat, perquè marca l'estil del codi modern que et trobaràs.

A favor de les comprovades:

  • Fan visible a la signatura què pot fallar. L'API es documenta sola.
  • Impedeixen oblidar una fallada previsible: el compilador no deixa compilar.
  • Obliguen a pensar en el camí d'error en el moment d'escriure el codi, no quan ja és en producció.

En contra:

  • Contaminen les signatures. Si un mètode profund llança IOException, tota la cadena fins a dalt l'ha de declarar o capturar. Un canvi intern es converteix en un canvi d'API.
  • Empenyen cap a l'antipatró. Davant de l'obligació, molta gent escriu el catch buit o el catch (Exception e) { e.printStackTrace(); }, que és pitjor que no haver capturat res: el programa continua amb l'estat trencat i sense que ningú se n'assabenti.
  • Trenquen amb les lambdes. Les interfícies funcionals de java.util.function que vas conèixer a 04-06 no declaren excepcions comprovades, així que no pots llançar una IOException des de dins d'un Function o un Consumer sense embolcallar-la. Això es va tornar molt incòmode a partir de Java 8, i és una de les raons de pes del gir modern cap a les no comprovades.
// Aixo NO compila: Consumer.accept no declara IOException
List<String> camins = List.of("cataleg.txt", "prestecs.txt");
camins.forEach(cami -> {
    Files.readString(Path.of(cami));    // error: unreported exception IOException
});

On és el consens avui: els frameworks moderns han votat amb els peus. Spring converteix totes les SQLException (comprovades) en la seva jerarquia DataAccessException (no comprovades). Hibernate fa el mateix. La majoria de llibreries noves fan servir exclusivament no comprovades.

La postura pragmàtica, que és la que seguiràs en aquest mòdul:

  • Fes servir no comprovades per defecte per als errors del teu domini.
  • Reserva les comprovades per a condicions externes on qui crida realment farà alguna cosa diferent de propagar.
  • Mai no silenciïs una comprovada amb un catch buit per treure-te-la de sobre. Si de debò no pots fer res útil allà, embolcalla-la en una de no comprovada conservant-ne la causa (06-03) i deixa que pugi.

  1. Els missatges de NullPointerException de Java 14+

La NullPointerException ha estat històricament l'excepció més frustrant de Java, per una raó molt concreta: no deia quina de les referències de la línia era null. Davant d'això:

int any = cataleg.cercarPerReferencia("LIB-0001").getFitxa().any();

...un NullPointerException en Java 8 et donava això:

Exception in thread "main" java.lang.NullPointerException
	at com.nexussoftware.bibliotech.presentacio.Informe.generar(Informe.java:42)

Era cataleg el nul? El resultat de cercarPerReferencia? El de getFitxa()? L'única sortida era partir la línia en diverses o obrir el depurador.

Des de Java 14, amb les helpful NullPointerExceptions (activades per defecte des de Java 15), el missatge és aquest:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke
"com.nexussoftware.bibliotech.domini.Fitxa.any()" because the return value of
"com.nexussoftware.bibliotech.servei.Cataleg.cercarPerReferencia(String)" is null
	at com.nexussoftware.bibliotech.presentacio.Informe.generar(Informe.java:42)

El missatge assenyala amb precisió quirúrgica: el valor retornat per cercarPerReferencia era null. Diagnòstic resolt sense depurador i sense tocar el codi.

La JVM sap construir aquests missatges per a tots els casos en què un null provoca la fallada:

Situació Fragment del missatge
Crida a mètode sobre null Cannot invoke "X.metode()" because "variable" is null
Lectura de camp Cannot read field "camp" because "variable" is null
Longitud d'array Cannot read the array length because "array" is null
Element d'array Cannot load from object array because "array" is null
Desempaquetatge d'embolcall Cannot invoke "java.lang.Integer.intValue()" because "obj" is null

Aquest últim és el que explica el clàssic NullPointerException "impossible" que et pot donar un Map<String, Integer>:

Map<String, Integer> prestecsPerEmpleat = new HashMap<>();
int total = prestecsPerEmpleat.get("EMP-999");   // la clau no existeix: get retorna null
// NullPointerException: Cannot invoke "java.lang.Integer.intValue()"
//                       because the return value of "java.util.Map.get(Object)" is null

El null retornat per get s'intenta desempaquetar a int (autounboxing, mòdul 1), cosa que implica cridar intValue() sobre null. El missatge detallat ho diu exactament. Sense ell, aquesta fallada desconcerta durant una bona estona.

Com que fas servir Java 17 o superior, tens això activat per defecte i de franc. Aprofita-ho: quan vegis una NullPointerException, llegeix el missatge complet abans de tocar res. Gairebé sempre t'està donant la resposta ja mastegada.

  1. Què no has de fer mai

Abans d'escriure el teu primer try a la lliçó següent, tres regles que no admeten matisos. Les veuràs demostrades amb codi a 06-02; aquí van els principis.

1. No capturis Throwable.

// PROHIBIT
try {
    gestor.prestar("LIB-0001", "EMP-001", 12);
} catch (Throwable t) {
    System.out.println("Alguna cosa ha fallat");
}

Capturar Throwable posa al mateix sac el teu error de negoci i un OutOfMemoryError o un StackOverflowError, dels quals no et pots recuperar. Encara pitjor: atrapa també ThreadDeath i errors del carregador de classes, amb els quals interferir pot deixar la JVM en un estat impossible de diagnosticar. Captura el tipus més específic que sàpigues gestionar.

2. No capturis i et quedis callat.

// PROHIBIT: el "catch buit", el pitjor error de la gestio d'excepcions
try {
    cataleg.registrar(material);
} catch (Exception e) {
    // ja ho mirarem
}

Això no gestiona l'error: el destrueix. L'excepció duia a dins la classe, el missatge, la causa i la pila completa; tot això es perd per sempre i el programa continua fingint que l'operació ha tingut èxit. Una fallada així pot trigar setmanes a manifestar-se, i quan ho faci no en quedarà ni rastre de l'origen. Si el catch és buit, és que estava malament capturar-la.

Un catch buit només és admissible en un cas, i amb comentari obligatori explicant per què la fallada és realment irrellevant allà.

3. No facis servir excepcions per al flux normal.

// PROHIBIT: recorrer una llista provocant l'excepcio de final
try {
    int i = 0;
    while (true) {
        System.out.println(materials.get(i++).getTitol());
    }
} catch (IndexOutOfBoundsException fi) {
    // "ja hem acabat la llista"
}

A part de ser il·legible, això és ordres de magnitud més lent que un for normal, perquè construir l'objecte excepció implica capturar la pila de crides completa. Les excepcions són per al que és excepcional. En veuràs els números a 06-02.

Errors Comuns i Consells

Creure que una excepció "trenca el programa" i prou. No: activa un mecanisme molt definit —desenrotllament de la pila buscant un gestor— que pots controlar completament. Només acaba el fil si ningú no la gestiona en tot el recorregut.

Llegir el stack trace de baix a dalt. Es llegeix de dalt a baix. La primera línia at és el punt exacte de la fallada; l'última és main. I si hi ha Caused by:, la veritat sol ser a l'últim d'ells.

Ignorar les línies del stack trace que no són teves. Correcte com a filtre inicial: busca la primera línia amb el teu paquet. Però no les esborris de l'informe d'error: el camí per dins de la llibreria de vegades és justament el que revela l'ús incorrecte.

Pensar que Exception inclou tot allò llançable. No inclou Error. catch (Exception e) no atrapa un OutOfMemoryError. En aquest cas concret, aquesta limitació és una virtut.

Confondre Error amb error. En Java, Error és una branca concreta de la jerarquia —fallades de la JVM—, no un sinònim genèric de "alguna cosa ha anat malament". Un NullPointerException no és un Error.

Creure que una excepció no comprovada és "menys greu". És exactament al revés: normalment indica un defecte en el teu codi, mentre que una de comprovada sol indicar una circumstància de l'entorn. La paraula "comprovada" descriu què fa el compilador, no la gravetat.

Escriure missatges d'excepció inútils. "Error", "Fallada" o "No s'ha pogut" no ajuden ningú a les tres de la matinada. Un bon missatge diu què ha passat, amb quina dada concreta i què s'esperava: "Referencia duplicada: LIB-0001 ja existeix al cataleg". És la mateixa quantitat de feina.

Consell: davant d'una NullPointerException, llegeix el missatge sencer. Des de Java 14 et diu literalment quina referència era nul·la. És la millora de diagnòstic més rendible de l'última dècada de Java.

Consell: quan reportis un error, copia el stack trace COMPLET. Inclosos tots els Caused by: i els ... N more. Un trace retallat a la primera línia sol eliminar precisament la informació que resolia el cas.

Consell: en producció, redirigeix System.err a un fitxer. Si no, els stack traces es perden tan bon punt es tanca el terminal. A 06-07 faràs això correctament, amb un Logger i un FileHandler.

Consell: -XX:-OmitStackTraceInFastThrow. Si en producció apareixen excepcions sense stack trace, és l'optimització fast throw del JIT. Aquest paràmetre la desactiva i retorna els traces complets mentre diagnostiques.

Exercicis

Exercici 1: el zoològic d'excepcions de BiblioTech

Escriu la classe ZoologicExcepcions al paquet com.nexussoftware.bibliotech.presentacio amb un main que, sense fer servir try ni catch (encara no toca), provoqui de manera controlada les excepcions següents, una per execució segons un argument numèric, i que abans de cadascuna imprimeixi què passarà i per què:

  1. NullPointerException en demanar el títol d'un material que cercarPerReferencia ha retornat com a null.
  2. ArrayIndexOutOfBoundsException en recórrer un array d'ISBN amb <= en comptes de <.
  3. NumberFormatException en convertir l'entrada "quinze dies" amb Integer.parseInt.
  4. ArithmeticException en calcular la mitjana de dies de préstec amb zero préstecs.
  5. ClassCastException en convertir un Object que conté un String a Integer.
  6. UnsupportedOperationException en afegir un material a una llista creada amb List.of.
  7. ConcurrentModificationException en eliminar d'una llista dins d'un for-each.

Per a cada cas, executa el programa i anota en un comentari al final del fitxer el missatge exacte que produeix cada excepció al teu JDK.

Exercici 2: lectura forense d'un stack trace

T'arriba aquest bolcat des de l'entorn de producció de Nexus Software:

Exception in thread "main" java.lang.IllegalStateException: No s'ha pogut generar el rebut del prestec PR-0007
	at com.nexussoftware.bibliotech.presentacio.RebutConsola.emetre(RebutConsola.java:54)
	at com.nexussoftware.bibliotech.servei.GestorPrestecs.retornar(GestorPrestecs.java:132)
	at com.nexussoftware.bibliotech.presentacio.MenuBiblioTech.opcioRetornar(MenuBiblioTech.java:188)
	at com.nexussoftware.bibliotech.presentacio.BiblioTechApp.main(BiblioTechApp.java:29)
Caused by: java.lang.NullPointerException: Cannot invoke "com.nexussoftware.bibliotech.domini.Empleat.getNom()" because "this.titular" is null
	at com.nexussoftware.bibliotech.domini.Prestec.descripcioTitular(Prestec.java:97)
	at com.nexussoftware.bibliotech.presentacio.RebutConsola.emetre(RebutConsola.java:51)
	... 3 more

Respon per escrit, justificant cada resposta amb la línia concreta del bolcat:

  1. Quin fil ha mort?
  2. Quina operació de negoci ha fallat, en llenguatge planer?
  3. Quina és la causa arrel exacta?
  4. En quin fitxer i línia començaries a investigar? Per què aquella i no la primera línia at?
  5. Què significa ... 3 more?
  6. Quants marcs tenia la pila en total en el moment de la fallada original?
  7. És aquest un error de programació o una condició de l'entorn? Quina branca de la jerarquia ho confirma?
  8. Proposa dues correccions: una que eviti el símptoma i una altra que ataqui la causa arrel. Quina és la bona?

Exercici 3: classificador d'excepcions

Escriu ClassificadorExcepcions, una utilitat que rebi un objecte Throwable ja construït (no llançat) i produeixi un informe amb:

  • El nom simple i el nom complet de la classe.
  • Si és un Error, una excepció comprovada o una no comprovada. Pista: t instanceof Error, t instanceof RuntimeException, i en cas contrari és comprovada.
  • La reacció recomanada, segons una taula que defineixis tu ("No gestionar: deixar morir el proces", "Corregir el codi", "Gestionar: es una condicio externa").
  • La cadena completa de causes, sagnada per nivells, recorrent amb getCause() fins a arribar a null.
  • El nombre de marcs de la seva pila i el primer marc que pertanyi al paquet com.nexussoftware, si n'hi ha.

Al main, construeix a mà una cadena de tres excepcions imbricades —una IllegalStateException causada per una RuntimeException causada per una java.io.IOException— i passa-la al classificador. Vigila amb les cadenes circulars: protegeix el recorregut amb un límit de profunditat.

Solucions

Solució 1

package com.nexussoftware.bibliotech.presentacio;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Provoca deliberadament les excepcions mes frequents, SENSE capturar-les,
 * per observar-ne el missatge i el stack trace.
 *
 * Us: java ZoologicExcepcions <1..7>
 */
public class ZoologicExcepcions {

    public static void main(String[] args) {
        int cas = (args.length > 0) ? Integer.parseInt(args[0]) : 1;
        System.out.println("=== Cas " + cas + " ===");

        switch (cas) {
            case 1 -> punterNul();
            case 2 -> indexForaDeRang();
            case 3 -> formatDeNumero();
            case 4 -> divisioPerZero();
            case 5 -> conversioInvalida();
            case 6 -> llistaImmutable();
            case 7 -> modificacioConcurrent();
            default -> System.out.println("Casos valids: 1 a 7");
        }

        // Aquesta linia NOMES s'imprimeix en el cas 'default': en els altres,
        // l'excepcio es propaga i acaba el fil main abans d'arribar aqui.
        System.out.println("Fi normal del programa.");
    }

    /** 1. NullPointerException: el cataleg retorna null quan no troba res. */
    private static void punterNul() {
        System.out.println("Cerquem LIB-9999, que no existeix. cercarPerReferencia retornara null");
        System.out.println("i en demanar-li el titol tindrem NullPointerException.");

        Map<String, String> index = new HashMap<>();
        index.put("LIB-0001", "Java Eficac");

        String titol = index.get("LIB-9999");        // null: la clau no existeix
        System.out.println(titol.toUpperCase());     // <-- NullPointerException
    }

    /** 2. ArrayIndexOutOfBoundsException: el classic error del <= al limit. */
    private static void indexForaDeRang() {
        System.out.println("Recorrem 3 ISBN amb i <= longitud en comptes de i < longitud.");

        String[] isbns = { "978-0000000001", "978-0000000002", "978-0000000003" };
        for (int i = 0; i <= isbns.length; i++) {   // <= : una volta de mes
            System.out.println("  ISBN[" + i + "] = " + isbns[i]);   // <-- explota amb i == 3
        }
    }

    /** 3. NumberFormatException: entrada d'usuari que no es un numero. */
    private static void formatDeNumero() {
        System.out.println("L'empleat va escriure 'quinze dies' on s'esperava un enter.");

        String entrada = "quinze dies";
        int dies = Integer.parseInt(entrada);       // <-- NumberFormatException
        System.out.println("Dies de prestec: " + dies);
    }

    /** 4. ArithmeticException: divisio ENTERA per zero. */
    private static void divisioPerZero() {
        System.out.println("Mitjana de dies amb zero prestecs: divisio entera per zero.");

        int diesTotals = 45;
        int nombrePrestecs = 0;

        // Avis: amb double NO hi hauria excepcio, donaria Infinity.
        System.out.println("  Amb double: " + (45.0 / 0));       // Infinity, sense fallada

        int mitjana = diesTotals / nombrePrestecs;               // <-- ArithmeticException
        System.out.println("Mitjana: " + mitjana);
    }

    /** 5. ClassCastException: conversio incompatible en temps d'execucio. */
    private static void conversioInvalida() {
        System.out.println("Un Object que conte un String es converteix a Integer.");

        Object dada = "Java Eficac";                // en realitat es un String
        Integer any = (Integer) dada;               // <-- ClassCastException
        System.out.println("Any: " + any);
    }

    /** 6. UnsupportedOperationException: List.of retorna una llista IMMUTABLE. */
    private static void llistaImmutable() {
        System.out.println("List.of crea una llista immutable; add llanca excepcio.");

        List<String> catalegFix = List.of("Java Eficac", "Patrons de Disseny");
        catalegFix.add("Refactoritzacio");          // <-- UnsupportedOperationException

        // Nota: Arrays.asList te el mateix problema per a add/remove,
        // encara que si que permet set(i, x). Es una vista de mida fixa sobre l'array.
        List<String> vista = Arrays.asList("a", "b");
        vista.set(0, "z");                          // aixo SI que funciona
    }

    /** 7. ConcurrentModificationException: modificar mentre s'itera (vist a 05-04). */
    private static void modificacioConcurrent() {
        System.out.println("Eliminem dins d'un for-each: l'iterador detecta el canvi.");

        List<String> materials = new ArrayList<>(
                List.of("Java Eficac", "Patrons de Disseny", "Refactoritzacio"));

        for (String titol : materials) {
            if (titol.startsWith("Patrons")) {
                materials.remove(titol);            // <-- ConcurrentModificationException
            }
        }
        // La forma correcta era: materials.removeIf(t -> t.startsWith("Patrons"));
    }
}

/*
 * MISSATGES OBSERVATS (JDK 17):
 *
 * 1. java.lang.NullPointerException: Cannot invoke "String.toUpperCase()"
 *      because the return value of "java.util.Map.get(Object)" is null
 * 2. java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
 * 3. java.lang.NumberFormatException: For input string: "quinze dies"
 * 4. java.lang.ArithmeticException: / by zero
 * 5. java.lang.ClassCastException: class java.lang.String cannot be cast to
 *      class java.lang.Integer (java.lang.String and java.lang.Integer are in
 *      module java.base of loader 'bootstrap')
 * 6. java.lang.UnsupportedOperationException     (sense missatge)
 * 7. java.util.ConcurrentModificationException   (sense missatge)
 *
 * Observacio: les excepcions 6 i 7 NO porten missatge. La seva sola classe es el
 * diagnostic. Es un recordatori que el nom de l'excepcio es la part mes
 * important de la informacio, i de per que a 06-04 es creen classes
 * propies amb noms del domini en lloc de reutilitzar tipus generics.
 */

Solució 2

1. Quin fil ha mort? El fil "main", segons la primera línia: Exception in thread "main". En ser l'únic fil del programa, la JVM acaba amb codi de sortida diferent de zero.

2. Quina operació de negoci ha fallat? Emetre el rebut de la devolució del préstec PR-0007. Ho diuen conjuntament el missatge de l'excepció de més alt nivell (No s'ha pogut generar el rebut del prestec PR-0007) i el camí de crides: mainopcioRetornarretornaremetre. L'usuari va triar l'opció "retornar" del menú.

3. Quina és la causa arrel exacta? El camp titular de l'objecte Prestec val null. Ho diu l'últim Caused by:, i amb la precisió dels missatges de Java 14+: Cannot invoke "...Empleat.getNom()" because "this.titular" is null. El this.titular indica que no és una variable local ni un paràmetre, sinó un camp del mateix Prestec: l'objecte es va construir sense titular o algú el va posar a null després.

4. On començar a investigar? A Prestec.java:97, dins de descripcioTitular, que és la primera línia at de l'últim Caused by:. Aquí és on el null es manifesta. No a la primera línia del bolcat (RebutConsola.java:54), perquè aquesta és només la capa que va embolcallar la fallada, no la que la va produir.

Ara bé, el lloc on es manifesta no és necessàriament on hi ha el defecte: el veritablement sospitós és com un Prestec va arribar a existir amb titular a null. La investigació real és al constructor de Prestec i a GestorPrestecs, que és qui els crea.

5. Què significa ... 3 more? Que els 3 marcs següents de la causa són idèntics als del bloc anterior i Java no els repeteix. Reconstruïts, són GestorPrestecs.retornar, MenuBiblioTech.opcioRetornar i BiblioTechApp.main. No hi ha informació perduda.

6. Quants marcs tenia la pila? Cinc. Els dos explícits de la causa (Prestec.descripcioTitular i RebutConsola.emetre) més els 3 comprimits al ... 3 more. La pila completa era:

Prestec.descripcioTitular   (Prestec.java:97)        ← CIM, aqui explota
RebutConsola.emetre         (RebutConsola.java:51)
GestorPrestecs.retornar     (GestorPrestecs.java:132)
MenuBiblioTech.opcioRetornar(MenuBiblioTech.java:188)
BiblioTechApp.main          (BiblioTechApp.java:29)  ← FONS

Detall fi: RebutConsola.emetre apareix a la línia 51 a la causa i a la 54 a l'embolcall. Coherent: a la 51 es va cridar descripcioTitular i va fallar; a la 54 hi ha el catch que la va embolcallar en la IllegalStateException.

7. Error de programació o condició de l'entorn? Error de programació. Ho confirma la branca de la jerarquia: NullPointerException descendeix de RuntimeException, és a dir, és no comprovada. Res extern no ha fallat —ni disc, ni xarxa, ni entrada d'usuari—: simplement existeix un objecte de domini en un estat invàlid que mai no s'hauria d'haver permès.

8. Dues correccions.

Correcció del símptoma (dolenta com a única solució):

// A Prestec.descripcioTitular
public String descripcioTitular() {
    if (titular == null) { return "(sense titular)"; }   // tapa el simptoma
    return titular.getNom();
}

El rebut s'emet, però sense titular, i el préstec continua estant corrupte en memòria i en qualsevol informe posterior. S'ha silenciat l'avís, no arreglat el problema.

Correcció de la causa (la bona): impedir que un Prestec pugui existir sense titular, validant al constructor —exactament el que faràs a 06-03:

public Prestec(String referencia, Material material, Empleat titular, int diaInici) {
    this.referencia = Objects.requireNonNull(referencia, "La referencia no pot ser nul-la");
    this.material   = Objects.requireNonNull(material,   "El material no pot ser nul");
    this.titular    = Objects.requireNonNull(titular,    "El titular no pot ser nul");
    // ...
}

Així la fallada es detecta en el moment i el lloc en què es crea l'objecte invàlid, amb un missatge clar, en comptes de vint minuts després en imprimir un rebut. És el principi de fallar ràpid (fail-fast) que desenvoluparàs a 06-03.

Solució 3

package com.nexussoftware.bibliotech.presentacio;

import java.io.IOException;

/**
 * Analitza un Throwable ja construit: branca de la jerarquia, reaccio
 * recomanada, cadena de causes i localitzacio del primer marc propi.
 *
 * No llanca ni captura res: nomes INSPECCIONA l'objecte excepcio,
 * demostrant que una excepcio es un objecte normal amb estat.
 */
public class ClassificadorExcepcions {

    /** Limit de seguretat: protegeix davant de cadenes de causes circulars. */
    private static final int PROFUNDITAT_MAXIMA = 10;

    private static final String PAQUET_PROPI = "com.nexussoftware";

    public static String informar(Throwable t) {
        if (t == null) {
            return "No hi ha excepcio que analitzar.";
        }

        StringBuilder sb = new StringBuilder();
        sb.append("========================================\n");
        sb.append("INFORME D'EXCEPCIO\n");
        sb.append("========================================\n");
        sb.append("Classe simple : ").append(t.getClass().getSimpleName()).append('\n');
        sb.append("Classe completa: ").append(t.getClass().getName()).append('\n');
        sb.append("Missatge      : ").append(t.getMessage()).append('\n');
        sb.append("Categoria     : ").append(categoria(t)).append('\n');
        sb.append("Reaccio       : ").append(reaccio(t)).append('\n');
        sb.append("Marcs de pila : ").append(t.getStackTrace().length).append('\n');
        sb.append("Primer marc propi: ").append(primerMarcPropi(t)).append('\n');
        sb.append("----------------------------------------\n");
        sb.append("CADENA DE CAUSES:\n");
        sb.append(cadenaDeCauses(t));
        sb.append("========================================");
        return sb.toString();
    }

    /**
     * Determina la branca de la jerarquia.
     * Compte amb l'ORDRE de les comprovacions: RuntimeException es filla d'Exception,
     * aixi que cal preguntar primer per la mes especifica. Es exactament
     * la mateixa regla d'ordre que governa els catch multiples (06-02).
     */
    private static String categoria(Throwable t) {
        if (t instanceof Error) {
            return "ERROR de la JVM (no comprovada)";
        }
        if (t instanceof RuntimeException) {
            return "NO COMPROVADA (RuntimeException)";
        }
        if (t instanceof Exception) {
            return "COMPROVADA (Exception, no RuntimeException)";
        }
        return "Throwable directe (cas molt rar)";
    }

    private static String reaccio(Throwable t) {
        if (t instanceof Error) {
            return "No gestionar. Deixar morir el proces, registrar i analitzar la causa.";
        }
        if (t instanceof RuntimeException) {
            return "Normalment indica un defecte: corregir el codi, no capturar-la.";
        }
        if (t instanceof Exception) {
            return "Condicio externa: gestionar-la (reintentar, degradar o informar).";
        }
        return "Sense recomanacio.";
    }

    /** Recorre getCause() amb sagnat creixent i proteccio anticircular. */
    private static String cadenaDeCauses(Throwable t) {
        StringBuilder sb = new StringBuilder();
        Throwable actual = t;
        int nivell = 0;

        while (actual != null && nivell < PROFUNDITAT_MAXIMA) {
            String sagnat = "  ".repeat(nivell);
            String prefix = (nivell == 0) ? "" : "Caused by: ";
            sb.append(sagnat)
              .append(prefix)
              .append(actual.getClass().getSimpleName())
              .append(": ")
              .append(actual.getMessage())
              .append('\n');

            Throwable seguent = actual.getCause();

            // Una excepcio pot ser la seva propia causa (Throwable ho permet si es
            // construeix malament): sense aquesta comprovacio, el bucle seria infinit.
            if (seguent == actual) {
                sb.append(sagnat).append("  [causa circular: s'atura aqui]\n");
                break;
            }
            actual = seguent;
            nivell++;
        }

        if (nivell >= PROFUNDITAT_MAXIMA) {
            sb.append("  [cadena truncada a ").append(PROFUNDITAT_MAXIMA).append(" nivells]\n");
        }
        if (nivell == 0) {
            sb.append("  (aquesta excepcio no te causa: es l'arrel)\n");
        }
        return sb.toString();
    }

    /**
     * Busca el primer marc que pertanyi al nostre codi.
     * Es el que fas mentalment en llegir un stack trace: saltar-te les linies
     * de java.base i de les llibreries fins a trobar la primera propia.
     */
    private static String primerMarcPropi(Throwable t) {
        for (StackTraceElement marc : t.getStackTrace()) {
            if (marc.getClassName().startsWith(PAQUET_PROPI)) {
                return marc.getClassName() + "." + marc.getMethodName()
                        + " (" + marc.getFileName() + ":" + marc.getLineNumber() + ")";
            }
        }
        return "(cap: l'excepcio es va originar integrament fora de " + PAQUET_PROPI + ")";
    }

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

    public static void main(String[] args) {
        // Construim a ma una cadena de tres nivells, de la mes profunda
        // a la mes superficial. El segon argument del constructor es la CAUSA.
        IOException arrel = new IOException("No s'ha pogut llegir cataleg.txt: permis denegat");

        RuntimeException intermedia =
                new RuntimeException("Ha fallat la carrega del cataleg des de disc", arrel);

        IllegalStateException superficial =
                new IllegalStateException("BiblioTech no pot arrencar sense cataleg", intermedia);

        System.out.println(informar(superficial));

        System.out.println();
        System.out.println(informar(arrel));

        System.out.println();
        System.out.println(informar(new StackOverflowError()));
    }
}

Sortida (abreujada):

========================================
INFORME D'EXCEPCIO
========================================
Classe simple : IllegalStateException
Classe completa: java.lang.IllegalStateException
Missatge      : BiblioTech no pot arrencar sense cataleg
Categoria     : NO COMPROVADA (RuntimeException)
Reaccio       : Normalment indica un defecte: corregir el codi, no capturar-la.
Marcs de pila : 1
Primer marc propi: com.nexussoftware.bibliotech.presentacio.ClassificadorExcepcions.main (ClassificadorExcepcions.java:141)
----------------------------------------
CADENA DE CAUSES:
IllegalStateException: BiblioTech no pot arrencar sense cataleg
  Caused by: RuntimeException: Ha fallat la carrega del cataleg des de disc
    Caused by: IOException: No s'ha pogut llegir cataleg.txt: permis denegat
========================================

Dos aprenentatges de l'exercici:

  1. L'ordre dels instanceof a categoria és obligatori. Si preguntessis primer t instanceof Exception, tota RuntimeException hi cauria i es classificaria malament, perquè RuntimeException és una Exception. Aquesta mateixa regla —del que és específic al que és general— serà la regla d'ordre dels blocs catch a 06-02, només que allà el compilador t'obligarà a respectar-la.
  2. La cadena de causes es recorre amb getCause() fins a null, i l'última de la cadena és la causa arrel. És exactament el que fa la JVM en imprimir els blocs Caused by:. En implementar-ho tu, entens que aquell format del stack trace no és màgia: és un recorregut trivial sobre una llista enllaçada d'excepcions.

Conclusió

Ja saps què és realment una excepció: un objecte que representa una fallada, que es construeix amb new com qualsevol altre, que duu a dins la seva classe, el seu missatge, la seva causa i una fotografia completa de la pila de crides, i que —quan es llança— interromp el mètode en sec i viatja cap enrere per la pila buscant qui se'n faci càrrec.

Entens per què això és millor que els codis de retorn: un false es pot ignorar en silenci, no diu per què ha fallat, ocupa el valor de retorn i obliga a comprovar a cada crida. El false mut de Cataleg.registrar i el null de cercarPerReferencia són, exactament, la primera fragilitat que aquest mòdul eliminarà.

Coneixes el mecanisme complet del desenrotllament de la pila: la JVM busca un gestor marc a marc, descartant cadascun sense executar la resta del seu codi, i si arriba al fons sense trobar-lo, lliura l'excepció al gestor per defecte del fil, que imprimeix el trace a System.err i acaba el fil —no necessàriament la JVM, matís que tornarà al mòdul 8—, deixant un codi de sortida diferent de zero.

Saps llegir un stack trace de dalt a baix, identificant el fil, la classe de l'excepció, el missatge i cada marc amb el seu fitxer i la seva línia; saps que la primera línia at és el punt de la fallada, que l'última és main, que convé saltar a la primera línia del teu propi paquet, i que davant d'una cadena de Caused by: la veritat és a l'últim. També saps que ... N more no amaga informació, i que un trace absent pot ser cosa de l'optimització fast throw.

Tens el mapa de la jerarquia: Throwable a l'arrel, amb Error per a les fallades de la JVM que no has de gestionarStackOverflowError, OutOfMemoryError—, i Exception per a les condicions anòmales, amb la seva branca RuntimeException d'errors de programació. Reconeixes les excepcions més freqüents de cada branca pel seu nom i pel seu missatge, saps que la divisió entera per zero explota mentre que la de coma flotant retorna Infinity en silenci, i saps que els missatges detallats de NullPointerException de Java 14+ et diuen literalment quina referència era nul·la.

I tens clara la distinció entre comprovades i no comprovades: què obliga el compilador en cada cas, el criteri de disseny —condició externa recuperable enfront de defecte del codi—, el debat real sobre el seu valor, i la postura pragmàtica que seguiràs a BiblioTech: no comprovades per defecte per al domini, comprovades només quan qui crida faci alguna cosa diferent de propagar. Juntament amb les tres prohibicions que no admeten matisos: no capturis Throwable, no capturis sense fer res, i no facis servir excepcions per al flux normal.

Fins aquí has après a llegir la fallada. A la lliçó següent, Bloc Try-Catch, aprendràs a gestionar-la: la sintaxi i la semàntica exactes de try i catch, què es protegeix i què se salta quan salta una excepció, els mètodes de l'objecte capturat, la regla d'ordre obligatòria quan hi ha diversos catch —de la subclasse a la superclasse, amb l'error de compilació que dona si la inverteixes—, el multi-catch amb | de Java 7 i les seves regles, l'abast de les variables declarades dins del try, i les quatre coses sensates que es poden fer dins d'un catch: recuperar-se amb un valor per defecte, reintentar, traduir a una altra excepció o registrar i rellançar. Amb la demostració dels antipatrons i del cost real de llançar una excepció enfront d'un simple if. I amb la primera victòria tangible del mòdul: el menú interactiu de BiblioTech deixarà de morir-se quan algú escrigui "dotze" on s'esperava un número.

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