La lliçó anterior va deixar dos deutes explícits. El primer: el patró manual de tancament de recursos, amb els seus set requisits i les seves vint línies de comptabilitat per a quatre de feina real. El segon, més greu: el finally que llança la seva pròpia excepció en tancar i fa desaparèixer la fallada original, deixant-te amb un missatge sobre el tancament i ni una pista del problema veritable.

Java 7 va resoldre totes dues de cop amb una sola construcció: el try-with-resources. Declares els recursos entre parèntesis, i el llenguatge s'encarrega de tancar-los sempre, en l'ordre correcte, i de conservar l'excepció original registrant la del tancament com a suprimida en comptes de deixar que la substitueixi.

Però aquesta lliçó no va només d'usar la construcció. Va d'entendre què fa exactament per sota —perquè la seva equivalència amb el try-finally manual és exacta i es pot escriure— i d'aprendre a fer que les teves pròpies classes siguin tancables. En acabar, BiblioTech tindrà una SessioBiblioteca que obre un torn de treball i, en tancar-se, consolida les estadístiques i allibera els bloquejos, se surti del bloc com se surti.

Avís d'abast: diversos exemples fan servir FileReader, BufferedReader i Scanner sobre fitxer. L'entrada/sortida és el mòdul 7, i allà s'explica aquella API a fons. Aquí es fa servir el mínim imprescindible, i el focus està sempre en el tancament, no en la lectura.

Contingut

  1. La sintaxi i la seva equivalència exacta
  2. AutoCloseable i Closeable
  3. Diversos recursos: ordre de tancament invers
  4. La variable del recurs és implícitament final
  5. La forma de Java 9: variables ja existents
  6. Excepcions suprimides: el problema resolt
  7. getSuppressed() i com apareixen al stack trace
  8. Combinar amb catch i finally
  9. Recursos que NO s'han de tancar així
  10. Implementar AutoCloseable a les teves classes
  11. Bones pràctiques a close()
  12. BiblioTech: SessioBiblioteca i ExportadorCataleg
  13. Taula resum: gestió manual enfront d'automàtica
  14. Errors Comuns i Consells
  15. Exercicis

  1. La sintaxi i la seva equivalència exacta

La forma és un try amb una llista de declaracions entre parèntesis:

try (Recurs r = new Recurs()) {
    // usar r
}
// r ja esta tancat aqui, passi el que passi

I ara, els dos codis en paral·lel. Aquest és el patró manual correcte de 06-05:

// ABANS DE JAVA 7: gestio manual
public int comptarLinies(String cami) throws IOException {
    BufferedReader lector = null;              // 1. declarar fora
    try {
        lector = new BufferedReader(new FileReader(cami));
        int linies = 0;
        while (lector.readLine() != null) {
            linies++;
        }
        return linies;
    } finally {
        if (lector != null) {                  // 2. comprovar null
            try {
                lector.close();                // 3. close() llanca IOException
            } catch (IOException eTancament) {
                // 4. no rellancar: perdriem l'excepcio original
                System.err.println("Avis: " + eTancament.getMessage());
            }
        }
    }
}

I aquest és l'equivalent amb try-with-resources:

// DES DE JAVA 7: gestio automatica
public int comptarLinies(String cami) throws IOException {
    try (BufferedReader lector = new BufferedReader(new FileReader(cami))) {
        int linies = 0;
        while (lector.readLine() != null) {
            linies++;
        }
        return linies;
    }
}

Nou línies de comptabilitat convertides en zero, i amb millor comportament que la versió manual, perquè l'excepció del tancament no es perd: queda registrada com a suprimida.

L'equivalència exacta que genera el compilador, per a un sol recurs, és aproximadament aquesta:

// El que el compilador genera (esquema simplificat)
BufferedReader lector = new BufferedReader(new FileReader(cami));
Throwable excepcioPrincipal = null;
try {
    // ... cos del try ...
} catch (Throwable t) {
    excepcioPrincipal = t;
    throw t;
} finally {
    if (lector != null) {
        if (excepcioPrincipal != null) {
            try {
                lector.close();
            } catch (Throwable tTancament) {
                excepcioPrincipal.addSuppressed(tTancament);  // <-- la clau
            }
        } else {
            lector.close();          // sense excepcio previa: si aquesta llanca, es propaga
        }
    }
}

Fixa't en la línia marcada: és exactament l'addSuppressed() que vas haver d'escriure a mà a l'exercici 3 de 06-05. Aquí l'hi posa el compilador per tu, en tots els casos i sense possibilitat d'oblidar-ho.

Un detall important d'aquella expansió: si no hi va haver excepció al cos, el close() es crida sense protecció, així que si el tancament falla, aquella excepció sí que es propaga. És el correcte: si res més no ha fallat, la fallada del tancament és l'única notícia i ha d'arribar a dalt.

  1. AutoCloseable i Closeable

Perquè una classe es pugui fer servir en un try-with-resources, ha d'implementar una d'aquestes dues interfícies:

// Java 7, java.lang
public interface AutoCloseable {
    void close() throws Exception;
}

// Java 5, java.io
public interface Closeable extends AutoCloseable {
    void close() throws IOException;
}

Les seves diferències:

AutoCloseable Closeable
Paquet java.lang java.io
Des de Java 7 Java 5
close() declara throws Exception throws IOException
Idempotència No exigida (però recomanada) Exigida pel contracte
Relació És la superinterfície Estén AutoCloseable
Quan triar-la Recursos generals: sessions, bloquejos, transaccions Recursos d'E/S

La diferència pràctica més important és l'excepció declarada. Si la teva classe implementa AutoCloseable sense restringir el throws, el compilador obligarà qui la faci servir a gestionar una Exception genèrica:

// close() declara Exception: l'usuari ha de capturar Exception, massa ample
class SessioAmpla implements AutoCloseable {
    @Override
    public void close() throws Exception { }
}

try (SessioAmpla s = new SessioAmpla()) {
    // ...
} catch (Exception e) {          // obligat a capturar Exception: horrible
    // ...
}

La solució, i és la pràctica correcta: restringeix el throws a la teva implementació. Recorda de 06-03 que una sobreescriptura pot declarar menys excepcions que el mètode original, mai més:

// BE: close() sense throws. Qui el faci servir no haura de capturar res.
class SessioNeta implements AutoCloseable {
    @Override
    public void close() {            // sense throws: mes restrictiu, perfectament legal
        // alliberar recursos sense llancar
    }
}

try (SessioNeta s = new SessioNeta()) {
    // ...
}
// Sense catch obligatori. Molt millor.

Regla pràctica: si el teu close() no necessita llançar una excepció comprovada, no la declaris. Cada throws que hi posis l'estàs imposant a tots els teus usuaris, a cada try-with-resources.

Aquestes són les classes de la biblioteca estàndard que ja són tancables i que et trobaràs:

Classe Interfície Mòdul del curs
BufferedReader, FileReader, FileWriter Closeable 7
Scanner Closeable 1 i 7
InputStream, OutputStream i les seves subclasses Closeable 7
Files.newBufferedReader(...) Closeable 7
Connection, Statement, ResultSet (JDBC) AutoCloseable 11
Socket, ServerSocket Closeable 9
ExecutorService (des de Java 19) AutoCloseable 8
Stream AutoCloseable 10

  1. Diversos recursos: ordre de tancament invers

Pots declarar diversos recursos separats per punt i coma. Es tanquen en ordre invers al d'obertura, que és el que necessites quan un recurs n'embolcalla un altre.

package com.nexussoftware.bibliotech.presentacio;

/**
 * Demostra l'ordre d'obertura i de tancament amb tres recursos.
 * Els recursos son simulats: no hi ha E/S real, perque el focus estigui
 * exclusivament en l'ORDRE.
 */
public class OrdreDeTancament {

    /** Recurs de joguina que anuncia la seva obertura i el seu tancament. */
    static class RecursTracat implements AutoCloseable {
        private final String nom;

        RecursTracat(String nom) {
            this.nom = nom;
            System.out.println("  OBRIR  " + nom);
        }

        void usar() {
            System.out.println("  USAR   " + nom);
        }

        @Override
        public void close() {                  // sense throws: mes comode per a qui l'usi
            System.out.println("  TANCAR " + nom);
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Sense excepcio ===");
        try (RecursTracat a = new RecursTracat("A-fitxer");
             RecursTracat b = new RecursTracat("B-buffer");
             RecursTracat c = new RecursTracat("C-parser")) {

            a.usar();
            b.usar();
            c.usar();
        }

        System.out.println("\n=== Amb excepcio al cos ===");
        try (RecursTracat a = new RecursTracat("A-fitxer");
             RecursTracat b = new RecursTracat("B-buffer")) {

            a.usar();
            throw new IllegalStateException("fallada al mig del proces");

        } catch (IllegalStateException e) {
            System.out.println("  CATCH  " + e.getMessage());
        }

        System.out.println("\n=== Amb excepcio en OBRIR el segon recurs ===");
        try (RecursTracat a = new RecursTracat("A-fitxer");
             RecursTracat b = crearQueFalla("B-buffer")) {
            a.usar();
        } catch (IllegalStateException e) {
            System.out.println("  CATCH  " + e.getMessage());
        }
    }

    static RecursTracat crearQueFalla(String nom) {
        throw new IllegalStateException("no s'ha pogut obrir " + nom);
    }
}

Sortida:

=== Sense excepcio ===
  OBRIR  A-fitxer
  OBRIR  B-buffer
  OBRIR  C-parser
  USAR   A-fitxer
  USAR   B-buffer
  USAR   C-parser
  TANCAR C-parser
  TANCAR B-buffer
  TANCAR A-fitxer

=== Amb excepcio al cos ===
  OBRIR  A-fitxer
  OBRIR  B-buffer
  USAR   A-fitxer
  TANCAR B-buffer
  TANCAR A-fitxer
  CATCH  fallada al mig del proces

=== Amb excepcio en OBRIR el segon recurs ===
  OBRIR  A-fitxer
  TANCAR A-fitxer
  CATCH  no s'ha pogut obrir B-buffer

Tres observacions clau:

El tancament és invers a l'obertura. C, B, A. És imprescindible quan els recursos s'embolcallen: un BufferedReader construït sobre un FileReader s'ha de tancar abans que el FileReader, perquè el seu close() buida la memòria intermèdia escrivint al flux subjacent. Si el tanquessis a l'inrevés, escriuries sobre un flux ja tancat.

Els recursos es tanquen abans que el catch. Al segon cas, TANCAR B i TANCAR A surten abans de CATCH. Això és el que vols: quan el gestor s'executa, els recursos ja estan alliberats i els pot fer servir per a una altra cosa, o reintentar.

Si falla l'obertura del segon recurs, el primer es tanca igualment. Aquest cas és el que el patró manual feia malament gairebé sempre: amb el try-finally de 06-05 calia imbricar un bloc per recurs per aconseguir-ho, i n'hi havia prou amb un descuit per deixar el primer obert.

  1. La variable del recurs és implícitament final

Dins del bloc, el recurs no es pot reassignar:

try (BufferedReader lector = new BufferedReader(new FileReader("cataleg.txt"))) {
    lector = new BufferedReader(new FileReader("altre.txt"));  // NO COMPILA
    // error: auto-closeable resource lector may not be assigned
}

La raó és evident si penses en l'expansió de l'apartat 1: el finally generat tanca la variable. Si la poguessis reassignar, tancaries el segon recurs i deixaries el primer obert per sempre. La restricció impedeix una fuita garantida.

Pots escriure final explícitament si t'agrada la claredat, encara que és redundant:

try (final BufferedReader lector = new BufferedReader(new FileReader(cami))) {
    // ...
}

I l'abast del recurs és el bloc try. Fora d'ell no existeix, ni tan sols als catch o al finally del mateix try:

try (RecursTracat r = new RecursTracat("A")) {
    r.usar();
} catch (Exception e) {
    r.usar();                      // NO COMPILA: cannot find symbol
} finally {
    r.usar();                      // NO COMPILA: cannot find symbol
}

Té sentit: quan s'executen el catch i el finally, el recurs ja està tancat. Que no sigui accessible t'impedeix fer-lo servir per error en aquell estat.

  1. La forma de Java 9: variables ja existents

A Java 7 i 8, el recurs s'havia de declarar dins dels parèntesis. Això obligava a un rodeig incòmode quan el recurs venia de fora:

// Java 7/8: cal crear una variable nova nomes per poder usar-la
public void processar(BufferedReader lectorRebut) throws IOException {
    try (BufferedReader lector = lectorRebut) {        // variable redundant
        System.out.println(lector.readLine());
    }
}

Des de Java 9 pots fer servir directament una variable final o efectivament final ja existent:

// Java 9+: es fa servir la variable tal qual
public void processar(BufferedReader lectorRebut) throws IOException {
    try (lectorRebut) {                                // sense declaracio nova
        System.out.println(lectorRebut.readLine());
    }
}

Efectivament final significa el mateix que a les lambdes de 04-05: una variable que no es reassigna després de la seva inicialització, encara que no dugui la paraula final.

public void demo() throws Exception {
    RecursTracat a = new RecursTracat("A");        // efectivament final: mai no es reassigna
    RecursTracat b = new RecursTracat("B");

    try (a; b) {                                    // diversos, separats per ;
        a.usar();
        b.usar();
    }
    // Es tanquen B i despres A
}

I si la variable que es reassigna, no compila:

RecursTracat r = new RecursTracat("A");
r = new RecursTracat("B");                          // reassignada: ja no es efectivament final

try (r) {                                            // NO COMPILA
    // error: local variables referenced from a resource specification
    //        must be final or effectively final
}

Un avís sobre quan fer servir aquesta forma. És còmoda, però comporta una decisió de disseny: qui és l'amo del recurs? Si un mètode rep un BufferedReader com a paràmetre i el tanca, està prenent una decisió que probablement correspon a qui l'hi va passar:

// SOSPITOS: tanco alguna cosa que no he obert jo
public void processar(BufferedReader lector) throws IOException {
    try (lector) {                     // tinc dret a tancar-lo?
        // ...
    }
}
// Qui cridi no podra tornar a usar el seu lector, i potser no s'ho esperava.

La regla general: tanca el que obres. Si reps un recurs ja obert, el normal és fer-lo servir i tornar-lo intacte, deixant el tancament a qui el va crear. La forma de Java 9 és més útil quan el recurs s'ha creat al mateix mètode però en diversos passos, o quan el mètode documenta explícitament que pren possessió del recurs.

  1. Excepcions suprimides: el problema resolt

Aquí és on try-with-resources demostra que és molt més que sucre sintàctic. Recupera el problema exacte de 06-05: el try llança la fallada real i el close() en llança una altra, i amb finally manual la segona esborrava la primera.

package com.nexussoftware.bibliotech.presentacio;

/**
 * Compara que passa quan fallen el cos I el tancament:
 *  - amb try-finally manual: l'excepcio del tancament SUBSTITUEIX l'original.
 *  - amb try-with-resources: l'original es conserva i la del tancament queda SUPRIMIDA.
 */
public class ExcepcionsSuprimides {

    /** Recurs el tancament del qual tambe falla. */
    static class RecursFragil implements AutoCloseable {
        private final String nom;

        RecursFragil(String nom) {
            this.nom = nom;
        }

        void llegir() {
            throw new IllegalStateException("FALLADA REAL: " + nom + " esta corrupte");
        }

        @Override
        public void close() {
            throw new IllegalStateException("FALLADA EN TANCAR: no s'ha alliberat " + nom);
        }
    }

    public static void main(String[] args) {

        System.out.println("=== A) try-finally MANUAL ===");
        try {
            ambFinallyManual();
        } catch (IllegalStateException e) {
            System.out.println("  Rebuda    : " + e.getMessage());
            System.out.println("  Causa     : " + e.getCause());
            System.out.println("  Suprimides: " + e.getSuppressed().length);
            System.out.println("  >> La FALLADA REAL ha desaparegut");
        }

        System.out.println("\n=== B) try-with-resources ===");
        try {
            ambTryWithResources();
        } catch (IllegalStateException e) {
            System.out.println("  Rebuda    : " + e.getMessage());
            System.out.println("  Causa     : " + e.getCause());
            System.out.println("  Suprimides: " + e.getSuppressed().length);
            for (Throwable suprimida : e.getSuppressed()) {
                System.out.println("    - " + suprimida.getMessage());
            }
            System.out.println("  >> Es conserven TOTES DUES");
        }

        System.out.println("\n=== C) Nomes falla el tancament ===");
        try {
            nomesFallaElTancament();
        } catch (IllegalStateException e) {
            System.out.println("  Rebuda    : " + e.getMessage());
            System.out.println("  Suprimides: " + e.getSuppressed().length);
            System.out.println("  >> Sense excepcio previa, la del tancament es propaga normal");
        }
    }

    static void ambFinallyManual() {
        RecursFragil recurs = new RecursFragil("cataleg.txt");
        try {
            recurs.llegir();
        } finally {
            recurs.close();           // aquesta excepcio SUBSTITUEIX la del try
        }
    }

    static void ambTryWithResources() {
        try (RecursFragil recurs = new RecursFragil("cataleg.txt")) {
            recurs.llegir();
        }
    }

    static void nomesFallaElTancament() {
        try (RecursFragil recurs = new RecursFragil("prestecs.txt")) {
            System.out.println("  (el cos acaba be)");
        }
    }
}

Sortida:

=== A) try-finally MANUAL ===
  Rebuda    : FALLADA EN TANCAR: no s'ha alliberat cataleg.txt
  Causa     : null
  Suprimides: 0
  >> La FALLADA REAL ha desaparegut

=== B) try-with-resources ===
  Rebuda    : FALLADA REAL: cataleg.txt esta corrupte
  Causa     : null
  Suprimides: 1
    - FALLADA EN TANCAR: no s'ha alliberat cataleg.txt
  >> Es conserven TOTES DUES

=== C) Nomes falla el tancament ===
  (el cos acaba be)
  Rebuda    : FALLADA EN TANCAR: no s'ha alliberat prestecs.txt
  Suprimides: 0
  >> Sense excepcio previa, la del tancament es propaga normal

Les regles del mecanisme de supressió, completes:

Situació Què es propaga Què queda suprimit
Falla només el cos La del cos Res
Falla només el tancament La del tancament Res
Fallen cos i tancament La del cos La del tancament
Fallen cos i diversos tancaments La del cos Totes les de tancament

La prioritat és deliberada i correcta: la fallada del cos és la que explica què ha anat malament; la del tancament sol ser una conseqüència. Però cap de les dues no es perd.

I un detall sobre la diferència entre causa i suprimida, que es confonen:

Causa (getCause) Suprimida (getSuppressed)
Relació A va provocar B A i B van passar independentment
Cardinalitat Una de sola, encadenada Diverses, en un array
S'estableix amb El constructor amb causa, o initCause addSuppressed(), gairebé sempre automàtic
Exemple NumberFormatException va causar IllegalStateException El fitxer estava corrupte i a més no es va poder tancar

  1. getSuppressed() i com apareixen al stack trace

Les excepcions suprimides apareixen al bolcat amb l'etiqueta Suppressed:, sagnades sota la principal:

Exception in thread "main" java.lang.IllegalStateException: FALLADA REAL: cataleg.txt esta corrupte
	at com.nexussoftware.bibliotech.presentacio.ExcepcionsSuprimides$RecursFragil.llegir(ExcepcionsSuprimides.java:22)
	at com.nexussoftware.bibliotech.presentacio.ExcepcionsSuprimides.ambTryWithResources(ExcepcionsSuprimides.java:70)
	at com.nexussoftware.bibliotech.presentacio.ExcepcionsSuprimides.main(ExcepcionsSuprimides.java:38)
	Suppressed: java.lang.IllegalStateException: FALLADA EN TANCAR: no s'ha alliberat cataleg.txt
		at com.nexussoftware.bibliotech.presentacio.ExcepcionsSuprimides$RecursFragil.close(ExcepcionsSuprimides.java:27)
		at com.nexussoftware.bibliotech.presentacio.ExcepcionsSuprimides.ambTryWithResources(ExcepcionsSuprimides.java:71)
		... 1 more

Com llegir-ho, ampliant el que vas aprendre a 06-01:

  • El bloc principal és la fallada que es va propagar.
  • Cada bloc Suppressed: (sagnat un nivell) és una fallada que va passar a més, típicament en tancar un recurs.
  • Cada bloc pot tenir al seu torn els seus propis Caused by: i les seves pròpies suprimides.

L'ordre de lectura recomanat, ara complet:

  1. Classe i missatge de l'excepció principal: què ha fallat.
  2. El seu últim Caused by:: la causa arrel d'aquella fallada.
  3. Les Suppressed:: què més ha fallat pel camí, normalment en alliberar recursos.

I un mètode d'utilitat que et serà útil per diagnosticar:

package com.nexussoftware.bibliotech.presentacio;

/** Bolca una excepcio amb les seves causes i les seves suprimides, sagnades. */
public final class InformeExcepcio {

    private InformeExcepcio() { }

    public static String descriure(Throwable t) {
        StringBuilder sb = new StringBuilder();
        descriure(t, sb, 0, "");
        return sb.toString();
    }

    private static void descriure(Throwable t, StringBuilder sb, int nivell, String etiqueta) {
        if (t == null || nivell > 10) {                // proteccio anticircular
            return;
        }
        String sagnat = "  ".repeat(nivell);
        sb.append(sagnat).append(etiqueta)
          .append(t.getClass().getSimpleName()).append(": ")
          .append(t.getMessage() != null ? t.getMessage() : "(sense missatge)")
          .append('\n');

        // Primer marc propi, per situar la fallada
        for (StackTraceElement m : t.getStackTrace()) {
            if (m.getClassName().startsWith("com.nexussoftware")) {
                sb.append(sagnat).append("    a ").append(m.getMethodName())
                  .append(" (").append(m.getFileName()).append(':')
                  .append(m.getLineNumber()).append(")\n");
                break;
            }
        }

        // Suprimides: van passar A MES (mateix nivell logic, sagnades)
        for (Throwable suprimida : t.getSuppressed()) {
            descriure(suprimida, sb, nivell + 1, "Suppressed: ");
        }

        // Causa: VA PROVOCAR l'anterior
        if (t.getCause() != null && t.getCause() != t) {
            descriure(t.getCause(), sb, nivell + 1, "Caused by: ");
        }
    }
}

  1. Combinar amb catch i finally

Un try-with-resources admet catch i finally com qualsevol altre try, i a diferència del try normal, pot anar sol: no necessita cap dels dos.

// Legal: try-with-resources sense catch ni finally
try (RecursTracat r = new RecursTracat("A")) {
    r.usar();
}

// Legal: amb catch
try (RecursTracat r = new RecursTracat("A")) {
    r.usar();
} catch (IllegalStateException e) {
    System.out.println("Fallada: " + e.getMessage());
}

// Legal: amb catch i finally
try (RecursTracat r = new RecursTracat("A")) {
    r.usar();
} catch (IllegalStateException e) {
    System.out.println("Fallada: " + e.getMessage());
} finally {
    System.out.println("Neteja addicional, no relacionada amb recursos");
}

L'ordre exacte d'execució quan hi són tots:

flowchart TB
    A["1. S'OBREN els recursos<br/>en l'ordre declarat"] --> B["2. S'executa el COS del try"]
    B --> C["3. Es TANQUEN els recursos<br/>en ordre INVERS"]
    C --> D{"Hi ha hagut excepcio?"}
    D -->|"si, i hi ha catch compatible"| E["4. S'executa el CATCH"]
    D -->|"no"| F["4. Se salta el catch"]
    E --> G["5. S'executa el FINALLY"]
    F --> G
    G --> H["6. Continua despres del bloc"]

L'essencial: els recursos es tanquen ABANS del catch i del finally. Quan el gestor s'executa, ja estan alliberats. Comprova-ho:

package com.nexussoftware.bibliotech.presentacio;

public class OrdreComplet {

    static class Recurs implements AutoCloseable {
        Recurs() { System.out.println("1. obrir"); }
        void usar() { throw new IllegalStateException("fallada al cos"); }
        @Override public void close() { System.out.println("3. tancar"); }
    }

    public static void main(String[] args) {
        try (Recurs r = new Recurs()) {
            System.out.println("2. cos");
            r.usar();
        } catch (IllegalStateException e) {
            System.out.println("4. catch: " + e.getMessage());
        } finally {
            System.out.println("5. finally");
        }
        System.out.println("6. despres");
    }
}

Sortida:

1. obrir
2. cos
3. tancar
4. catch: fallada al cos
5. finally
6. despres

I per a què serveix un finally aquí, si els recursos ja es tanquen sols? Per a neteja que no sigui de recursos: restaurar un marcador, restituir un estat, registrar la durada de l'operació. És a dir, exactament el patró de compensació de 06-05, que continua sent necessari i que ara conviu amb la gestió automàtica de recursos.

  1. Recursos que NO s'han de tancar així

try-with-resources tanca el recurs sempre, i això de vegades és justament el que no vols.

El cas més important: System.in.

// PROBLEMA REAL: tanca System.in per a TOTA l'aplicacio
public int llegirOpcio() {
    try (Scanner scanner = new Scanner(System.in)) {     // MALAMENT
        return Integer.parseInt(scanner.nextLine());
    }
}

// A partir d'aqui, qualsevol altre Scanner sobre System.in falla:
// NoSuchElementException: No line found

Scanner implementa Closeable, i el seu close() tanca el flux subjacent. Si aquell flux és System.in, el tanques per a tot el procés, no només per al teu mètode. El següent Scanner que algú creï sobre System.in llançarà NoSuchElementException a la primera lectura, i la fallada apareixerà lluny del mètode culpable.

Aquest és exactament l'error que va evitar el mòdul 1 amb la seva convenció d'un sol Scanner compartit, creat una vegada i mai tancat:

// CORRECTE: un unic Scanner d'instancia, sense try-with-resources
public class MenuBiblioTech {

    private final Scanner scanner = new Scanner(System.in);   // es crea una vegada

    public int llegirOpcio() {
        // Sense try-with-resources: System.in NO es tanca
        return Integer.parseInt(scanner.nextLine().trim());
    }
    // No es tanca mai: System.in el tanca la JVM en acabar el proces
}

La regla general, que ja has vist enunciada a la forma de Java 9:

Tanca el que obres. Si el recurs te'l van donar ja obert, o és un recurs global del procés, no n'ets l'amo i no l'has de tancar.

Altres casos en què no s'ha de fer servir try-with-resources:

Recurs Per què no
System.in, System.out, System.err Són globals del procés. Tancar-los afecta tota l'aplicació
Un recurs rebut com a paràmetre L'amo és qui el va obrir; probablement el vulgui continuar fent servir
Un recurs que es retorna a qui crida El tancaries abans que qui el rep el pugui fer servir
Recursos d'un pool (connexions gestionades) Amb un pool, close() normalment retorna la connexió al pool, així que allà sí que és correcte: comprova la documentació
Un recurs que ha de sobreviure al mètode Guarda'l com a camp i tanca'l al close() de la teva pròpia classe

El tercer cas mereix una nota, perquè produeix un error molt subtil:

// MALAMENT: es tanca el recurs just abans de retornar-lo
public BufferedReader obrirCataleg(String cami) throws IOException {
    try (BufferedReader lector = new BufferedReader(new FileReader(cami))) {
        return lector;             // es tanca ABANS de retornar (06-05: el finally corre primer)
    }
}
// Qui el rebi obtindra "Stream closed" a la primera lectura.

Si un mètode retorna un recurs, no el pot tancar: la responsabilitat passa a qui crida, i això s'ha de documentar clarament.

  1. Implementar AutoCloseable a les teves classes

Les teves pròpies classes poden participar en try-with-resources. És la forma idiomàtica de modelar qualsevol cosa amb un cicle de vida obrir → usar → tancar: una sessió, un torn de treball, una transacció, un bloqueig, una connexió.

L'esquelet:

public class ElMeuRecurs implements AutoCloseable {

    private boolean tancat = false;

    public ElMeuRecurs() {
        // adquirir el que calgui
    }

    public void operacio() {
        comprovarObert();            // protegir davant de l'us despres del tancament
        // ...
    }

    private void comprovarObert() {
        if (tancat) {
            throw new IllegalStateException("El recurs ja esta tancat");
        }
    }

    @Override
    public void close() {            // sense throws: mes comode per a qui l'usi
        if (tancat) {
            return;                  // IDEMPOTENT: tancar dues vegades no falla
        }
        tancat = true;
        // alliberar el que s'ha adquirit
    }
}

Els quatre elements que no han de faltar:

  1. Un marcador tancat, per saber en quin estat està.
  2. close() idempotent: cridar-lo dues vegades no falla ni fa la feina dues vegades.
  3. Comprovació als mètodes d'ús, que llança IllegalStateException si ja està tancat (06-03: el problema és quan m'ho demanes).
  4. close() sense throws comprovat si ho pots evitar.

  1. Bones pràctiques a close()

El contracte de close() té regles que convé respectar, perquè qui faci servir la teva classe les donarà per suposades.

1. Ha de ser idempotent. Closeable ho exigeix literalment: "si el flux ja està tancat, invocar aquest mètode no té efecte". AutoCloseable només ho recomana, però compleix-ho igualment. Sense idempotència, aquest codi explota:

try (ElMeuRecurs r = new ElMeuRecurs()) {
    r.operacio();
    r.close();                // tancament explicit anticipat
}                             // ...i el try-with-resources el tanca UNA ALTRA VEGADA

2. No ha de llançar si es pot evitar. Una excepció a close() és especialment traïdora: si el cos també ha fallat, la teva queda suprimida i probablement ningú no la llegeixi; i si el cos ha anat bé, la teva excepció converteix una operació reeixida en una fallada. El raonable en la majoria dels casos és registrar el problema i no propagar-lo:

@Override
public void close() {
    if (tancat) { return; }
    tancat = true;
    try {
        alliberarRecursSubjacent();
    } catch (RuntimeException e) {
        // Es registra, no es propaga: tancar no ha de trencar una operacio correcta.
        // A 06-07 aixo sera logger.log(Level.WARNING, "...", e).
        System.err.println("Avis: fallada en alliberar el recurs: " + e.getMessage());
    }
}

Amb una excepció important: si close() és el moment en què les dades es confirmen, llavors sí que ha de llançar. Un BufferedWriter.close() buida la memòria intermèdia al disc; si això falla, les dades no s'han escrit i qui crida se n'ha d'assabentar. Silenciar aquella fallada seria pitjor que propagar-la.

3. Ha de ser ràpid. close() s'executa al camí de sortida, sovint mentre es propaga una excepció. No és lloc per a operacions llargues ni per a lògica de negoci pesada.

4. Ha de deixar l'objecte inutilitzable, no mig usable. Després de close(), els mètodes d'ús han de llançar IllegalStateException, no comportar-se de manera estranya.

5. No depenguis de finalize(). El vell mecanisme de finalització està obsolet des de Java 9 i marcat per a eliminació: no hi ha cap garantia de quan —ni de si— s'executarà. AutoCloseable amb try-with-resources és el mecanisme correcte i determinista. (Cleaner és l'alternativa moderna per a casos avançats, i queda fora de l'abast d'aquest curs.)

  1. BiblioTech: SessioBiblioteca i ExportadorCataleg

Primer, la sessió de treball. Un empleat obre un torn, fa operacions, i en tancar-lo es consoliden les estadístiques i s'alliberen els bloquejos. Se surti com se surti del bloc.

package com.nexussoftware.bibliotech.servei;

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

/**
 * Torn de treball d'un empleat sobre BiblioTech.
 *
 * En tancar-se:
 *   1. consolida les estadistiques del torn,
 *   2. allibera el bloqueig del cataleg,
 *   3. deixa la sessio inutilitzable.
 *
 * Implementa AutoCloseable, no Closeable, perque no es un recurs d'E/S.
 * I el seu close() NO declara throws: aixi qui la usi no esta obligat a capturar res.
 */
public class SessioBiblioteca implements AutoCloseable {

    private final String idEmpleat;
    private final int diaObertura;
    private final Cataleg cataleg;

    private final List<String> operacions = new ArrayList<>();
    private int prestecsRealitzats = 0;
    private int devolucionsRealitzades = 0;
    private double multesCobrades = 0.0;

    private boolean tancada = false;

    /** Estadistiques acumulades de tots els torns (estatiques: 03-02). */
    private static int tornsCompletats = 0;
    private static int operacionsTotals = 0;

    public SessioBiblioteca(String idEmpleat, int diaObertura, Cataleg cataleg) {
        this.idEmpleat = Objects.requireNonNull(idEmpleat, "L'empleat no pot ser nul");
        this.cataleg = Objects.requireNonNull(cataleg, "El cataleg no pot ser nul");
        if (diaObertura < 1) {
            throw new IllegalArgumentException("El dia ha de ser 1 o posterior, i era: " + diaObertura);
        }
        this.diaObertura = diaObertura;

        cataleg.bloquejar(idEmpleat);           // adquisicio del recurs
        System.out.println("  [sessio de " + idEmpleat + " oberta el dia " + diaObertura + "]");
    }

    // ---------- Operacions del torn ----------

    public void anotarPrestec(String referencia) {
        comprovarOberta();
        prestecsRealitzats++;
        operacions.add("PRESTEC " + referencia);
    }

    public void anotarDevolucio(String referencia, double multa) {
        comprovarOberta();
        if (multa < 0) {
            throw new IllegalArgumentException("La multa no pot ser negativa: " + multa);
        }
        devolucionsRealitzades++;
        multesCobrades += multa;
        operacions.add(String.format("DEVOLUCIO %s (multa %.2f)", referencia, multa));
    }

    public List<String> getOperacions() {
        comprovarOberta();
        return List.copyOf(operacions);
    }

    /**
     * Tots els metodes d'us comproven l'estat.
     * Es IllegalStateException, no IllegalArgumentException: el problema
     * es QUAN es demana, no que es demana (06-03).
     */
    private void comprovarOberta() {
        if (tancada) {
            throw new IllegalStateException(
                    "La sessio de " + idEmpleat + " ja esta tancada");
        }
    }

    // ---------- Tancament ----------

    /**
     * Consolida i allibera. IDEMPOTENT i sense throws comprovat.
     *
     * S'executa amb exit i amb fallada del cos, perque el try-with-resources
     * ho garanteix igual que un finally (06-05).
     */
    @Override
    public void close() {
        if (tancada) {
            return;                              // idempotent
        }
        tancada = true;

        // 1. Consolidar estadistiques
        tornsCompletats++;
        operacionsTotals += operacions.size();

        System.out.printf("  [sessio de %s tancada] %d prestecs, %d devolucions, %.2f EUR%n",
                idEmpleat, prestecsRealitzats, devolucionsRealitzades, multesCobrades);

        // 2. Alliberar el bloqueig, protegit per no trencar una operacio correcta
        try {
            cataleg.desbloquejar(idEmpleat);
        } catch (RuntimeException e) {
            // No es propaga: tancar no ha de convertir un torn correcte en una fallada.
            // A 06-07 aixo sera logger.log(Level.WARNING, ..., e).
            System.err.println("  Avis: no s'ha pogut alliberar el bloqueig del cataleg: "
                    + e.getMessage());
        }
    }

    public boolean estaTancada() { return tancada; }

    public static int getTornsCompletats()  { return tornsCompletats; }
    public static int getOperacionsTotals() { return operacionsTotals; }
}

Ara l'exportador, que sí que toca un fitxer i serveix per veure dos recursos imbricats:

package com.nexussoftware.bibliotech.servei;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;

import com.nexussoftware.bibliotech.domini.Material;

/**
 * Exporta el cataleg a un fitxer de text.
 *
 * Implementa Closeable (no AutoCloseable) perque ES un recurs d'E/S i el seu
 * close() pot llancar IOException legitimament: en tancar es buida la
 * memoria intermedia al disc, i si aixo falla, les dades NO s'han escrit. Aquella
 * fallada SI que ha d'arribar a qui crida.
 *
 * (L'API d'escriptura es desenvolupa al modul 7.)
 */
public class ExportadorCataleg implements java.io.Closeable {

    private final String cami;
    private final BufferedWriter escriptor;

    private int materialsEscrits = 0;
    private boolean tancat = false;

    public ExportadorCataleg(String cami) throws IOException {
        this.cami = cami;
        this.escriptor = new BufferedWriter(new FileWriter(cami));
        escriureCapcalera();
    }

    private void escriureCapcalera() throws IOException {
        escriptor.write("# Cataleg de BiblioTech - Nexus Software");
        escriptor.newLine();
        escriptor.write("# referencia;titol;disponible");
        escriptor.newLine();
    }

    public void exportar(Material material) throws IOException {
        if (tancat) {
            throw new IllegalStateException("L'exportador de '" + cami + "' ja esta tancat");
        }
        escriptor.write(material.getReferencia() + ";" + material.getTitol() + ";"
                + material.estaDisponible());
        escriptor.newLine();
        materialsEscrits++;
    }

    public void exportarTots(List<Material> materials) throws IOException {
        for (Material m : materials) {
            exportar(m);
        }
    }

    public int getMaterialsEscrits() { return materialsEscrits; }

    /**
     * Tanca el fitxer.
     *
     * AQUI SI que es propaga la IOException: el close() d'un escriptor buida la
     * memoria intermedia al disc. Si falla, les dades no estan guardades i qui crida
     * se n'ha d'assabentar. Es l'excepcio a la regla de "close no llanca".
     */
    @Override
    public void close() throws IOException {
        if (tancat) {
            return;                              // idempotent
        }
        tancat = true;
        try {
            escriptor.write("# Total: " + materialsEscrits + " materials");
            escriptor.newLine();
        } finally {
            escriptor.close();                   // buida la memoria intermedia i allibera el descriptor
        }
    }
}

I la demostració conjunta:

package com.nexussoftware.bibliotech.presentacio;

import java.io.IOException;

import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.servei.ExportadorCataleg;
import com.nexussoftware.bibliotech.servei.SessioBiblioteca;

public class DemoSessioIExportador {

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

        // === 1. Torn que va be ===
        System.out.println("=== 1. Torn correcte de Marta ===");
        try (SessioBiblioteca sessio = new SessioBiblioteca("EMP-001", 10, cataleg)) {
            sessio.anotarPrestec("LIB-0001");
            sessio.anotarPrestec("LIB-0002");
            sessio.anotarDevolucio("LIB-0003", 1.25);
            System.out.println("  Operacions del torn: " + sessio.getOperacions());
        }
        System.out.println("  Cataleg bloquejat: " + cataleg.estaBloquejat());

        // === 2. Torn que falla a mitges ===
        System.out.println("\n=== 2. Torn de Diego que falla ===");
        try (SessioBiblioteca sessio = new SessioBiblioteca("EMP-002", 11, cataleg)) {
            sessio.anotarPrestec("LIB-0001");
            sessio.anotarDevolucio("LIB-0002", -5.0);        // llanca
            System.out.println("  aquesta linia no s'executa");
        } catch (IllegalArgumentException e) {
            System.out.println("  Fallada capturada: " + e.getMessage());
        }
        System.out.println("  Cataleg bloquejat: " + cataleg.estaBloquejat());
        System.out.println("  (la sessio s'ha tancat i consolidat igualment)");

        // === 3. Usar la sessio despres de tancar-la ===
        System.out.println("\n=== 3. Us despres del tancament ===");
        SessioBiblioteca fora;
        try (SessioBiblioteca sessio = new SessioBiblioteca("EMP-003", 12, cataleg)) {
            sessio.anotarPrestec("LIB-0003");
            fora = sessio;                                    // es guarda la referencia
        }
        try {
            fora.anotarPrestec("LIB-0001");
        } catch (IllegalStateException e) {
            System.out.println("  " + e.getMessage());
        }
        System.out.println("  close() dues vegades: ");
        fora.close();                                         // idempotent: no falla
        System.out.println("  sense errors");

        // === 4. Dos recursos imbricats ===
        System.out.println("\n=== 4. Sessio + exportador (dos recursos) ===");
        try (SessioBiblioteca sessio = new SessioBiblioteca("EMP-001", 13, cataleg);
             ExportadorCataleg exportador = new ExportadorCataleg("cataleg-export.txt")) {

            exportador.exportarTots(cataleg.llistar());
            sessio.anotarPrestec("LIB-0002");
            System.out.println("  Exportats: " + exportador.getMaterialsEscrits());

        } catch (IOException e) {
            System.out.println("  Fallada d'exportacio: " + e.getMessage());
        }
        // Tancament en ordre INVERS: primer l'exportador, despres la sessio

        System.out.println("\n=== Estadistiques globals ===");
        System.out.println("  Torns completats  : " + SessioBiblioteca.getTornsCompletats());
        System.out.println("  Operacions totals : " + SessioBiblioteca.getOperacionsTotals());
    }
}

Sortida:

=== 1. Torn correcte de Marta ===
  [sessio de EMP-001 oberta el dia 10]
  Operacions del torn: [PRESTEC LIB-0001, PRESTEC LIB-0002, DEVOLUCIO LIB-0003 (multa 1,25)]
  [sessio de EMP-001 tancada] 2 prestecs, 1 devolucions, 1,25 EUR
  Cataleg bloquejat: false

=== 2. Torn de Diego que falla ===
  [sessio de EMP-002 oberta el dia 11]
  [sessio de EMP-002 tancada] 1 prestecs, 0 devolucions, 0,00 EUR
  Fallada capturada: La multa no pot ser negativa: -5.0
  Cataleg bloquejat: false
  (la sessio s'ha tancat i consolidat igualment)

=== 3. Us despres del tancament ===
  [sessio de EMP-003 oberta el dia 12]
  [sessio de EMP-003 tancada] 1 prestecs, 0 devolucions, 0,00 EUR
  La sessio de EMP-003 ja esta tancada
  close() dues vegades:
  sense errors

=== 4. Sessio + exportador (dos recursos) ===
  [sessio de EMP-001 oberta el dia 13]
  Exportats: 3
  [sessio de EMP-001 tancada] 1 prestecs, 0 devolucions, 0,00 EUR

=== Estadistiques globals ===
  Torns completats  : 4
  Operacions totals : 5

El que demostra el cas 2 és el cor de la lliçó: el torn de Diego va fallar a mitges, i tanmateix la sessió es va tancar, les estadístiques es van consolidar amb el que sí que s'havia fet, el bloqueig es va alliberar i l'excepció va arribar al catch amb el seu missatge original. Sense una sola línia de finally escrita a mà.

I el cas 4 mostra l'ordre de tancament invers: l'exportador —declarat el segon— es tanca primer, i la sessió després.

  1. Taula resum: gestió manual enfront d'automàtica

Aspecte try-finally manual try-with-resources
Declaració de la variable Fora del try, inicialitzada a null Dins dels parèntesis
Comprovació de null Manual, obligatòria Automàtica
Crida a close() Manual, al finally Automàtica
close() que llança comprovada Un altre try imbricat Automàtic
Ordre amb diversos recursos Manual, i cal imbricar Automàtic i invers
Fallada en obrir el segon recurs El primer es queda obert si no imbriques El primer es tanca sempre
Excepció del cos + del tancament La del tancament esborra l'original L'original es conserva; la del tancament queda suprimida
Línies per a dos recursos ~20 2
Probabilitat de fer-ho malament Alta Nul·la
Quan continua calent finally Per a compensació d'estat, no de recursos

La conclusió operativa: en codi nou, sempre try-with-resources per a qualsevol cosa que es tanqui. El try-finally continua sent necessari, però per a una altra cosa: restaurar estat i compensar operacions a mitges, com a 06-05.

Errors Comuns i Consells

Embolcallar System.in en un try-with-resources. Tanca l'entrada estàndard per a tota l'aplicació, i el següent Scanner llançarà NoSuchElementException des d'un lloc que no en té la culpa. Un sol Scanner compartit, creat una vegada, mai tancat.

Tancar un recurs que t'han passat com a paràmetre. No n'ets l'amo. Qui te'l va donar probablement el vulgui continuar fent servir. Tanca el que obres.

Retornar un recurs des de dins del try-with-resources. Es tanca abans de retornar —el tancament corre abans que el return, igual que el finally de 06-05— i qui el rebi obtindrà Stream closed a la primera operació.

Declarar throws Exception al teu close(). L'imposes a tots els teus usuaris a cada try-with-resources. Restringeix el throws al que realment llancis, o a res.

close() no idempotent. Si algú tanca explícitament dins del bloc, el tancament automàtic el cridarà un altre cop. Amb el marcador tancat i un return primerenc, resolt.

Llançar des de close() quan el cos ha anat bé. Converteixes una operació correcta en una fallada. Registra i no propaguis, tret de quan close() sigui el moment en què les dades es confirmen —un escriptor que buida la seva memòria intermèdia—, perquè llavors la fallada és real i cal donar-la.

Buscar l'excepció del tancament a getCause(). No hi és: és a getSuppressed(). getCause() és A va provocar B; getSuppressed() és A i B van passar a més.

Reassignar la variable del recurs. No compila, i és una bona notícia: si es pogués, deixaries el primer recurs obert per sempre.

Fer servir el recurs al catch o al finally del mateix try. No és a l'abast —i encara que hi fos, ja està tancat.

Confiar en finalize() per alliberar recursos. Està obsolet des de Java 9 i marcat per a eliminació, i no garanteix quan ni si s'executa. AutoCloseable és determinista.

Consell: qualsevol cicle de vida obrir/usar/tancar mereix ser AutoCloseable. Sessions, torns, bloquejos, transaccions, temporitzadors. El bloc try documenta visualment l'abast de vida del recurs.

Consell: tria Closeable per a E/S i AutoCloseable per a tota la resta. I a AutoCloseable, restringeix el throws.

Consell: comprova l'estat al principi de cada mètode d'ús. Un IllegalStateException clar és infinitament millor que un comportament rar sobre un recurs ja tancat.

Consell: si tens un try-finally el finally del qual només crida close(), converteix-lo. Guanyes la gestió correcta de les excepcions de tancament de franc.

Exercicis

Exercici 1: BloqueigCataleg reentrant i tancable

Escriu BloqueigCataleg a com.nexussoftware.bibliotech.servei: un bloqueig d'exclusió sobre el catàleg que s'adquireix en construir-se i s'allibera en tancar-se.

Requisits:

  • Implementa AutoCloseable amb close() sense throws.
  • Camps: idPropietari, momentAdquisicio (un comptador incremental, no LocalDate), tancat.
  • Un camp estàtic propietariActual (String, null si lliure) i bloqueigsConcedits (comptador).
  • El constructor llança IllegalStateException si ja hi ha un altre propietari, amb un missatge que digui qui el té.
  • És reentrant: si el mateix propietari el torna a demanar, es concedeix i es porta el compte de la profunditat; el bloqueig només s'allibera de debò quan es tanca el més extern.
  • close() idempotent.
  • Mètode void comprovarPropietat(String id) que llanci IllegalStateException si id no és el propietari actual.

Al main, demostra: adquisició i alliberament normals, reentrància amb dos nivells imbricats, un intent d'adquisició per un altre empleat que falla, l'alliberament garantit després d'una excepció al cos, i el close() doble.

Exercici 2: excepcions suprimides en cascada

Escriu CascadaDeFallades amb una classe RecursQueFalla implements AutoCloseable el constructor de la qual rebi el nom i dos marcadors: si ha de fallar en usar-se i si ha de fallar en tancar-se.

Escriu un main que executi cinc escenaris amb tres recursos declarats al mateix try-with-resources, i per a cadascun mostri l'excepció principal, la seva causa i totes les seves suprimides amb getSuppressed():

  1. No falla res.
  2. Falla només el cos.
  3. Falla només el tancament del recurs central.
  4. Falla el cos i el tancament dels tres recursos.
  5. Falla l'obertura del tercer recurs (llança al constructor).

Per a cada escenari, respon per escrit: quina excepció es propaga?, quantes queden suprimides?, en quin ordre apareixen les suprimides i per què?, quins recursos van arribar a tancar-se?

Afegeix un mètode bolcar(Throwable t) que imprimeixi l'arbre complet de causes i suprimides amb sagnat.

Exercici 3: ImportadorCataleg amb recursos reals

Escriu ImportadorCataleg que llegeixi un fitxer de text amb línies referencia;titol;autor;any;isbn i registri els materials al Cataleg, amb un informe d'importació.

Requisits:

  • Fes servir try-with-resources amb un BufferedReader sobre un FileReader. Documenta amb un comentari que l'API d'E/S és el mòdul 7 i limita'n l'ús a obrir, llegir línies i tancar.
  • El mètode InformeImportacio importar(String cami) throws IOException no captura la IOException d'obertura: la declara perquè decideixi qui crida.
  • Cada línia es processa de manera independent: les fallades d'una no impedeixen les altres. Distingeix almenys tres motius de rebuig, un d'ells capturant ReferenciaDuplicadaException de 06-04.
  • Abans de llegir, l'importador ha de crear un fitxer d'exemple si no existeix, fent servir ExportadorCataleg o un BufferedWriter al seu propi try-with-resources.
  • Afegeix un mètode importarAmbSessio(String cami, String idEmpleat, Cataleg cataleg) que combini dos recursos: una SessioBiblioteca i el BufferedReader, i demostri l'ordre de tancament invers.
  • El main ha de demostrar: una importació correcta, una importació d'un fitxer inexistent (FileNotFoundException propagada i gestionada a dalt), i l'informe final.

Solucions

Solució 1

package com.nexussoftware.bibliotech.servei;

import java.util.Objects;

/**
 * Bloqueig d'exclusio sobre el cataleg, reentrant i tancable.
 *
 * Us previst:
 *   try (BloqueigCataleg b = new BloqueigCataleg("EMP-001")) {
 *       // operacions exclusives
 *   }   // alliberat sempre
 *
 * Nota: aixo NO es segur per a concurrencia; els bloqueigs reals entre
 * fils son materia del modul 8. Aqui es un bloqueig logic d'un sol fil.
 */
public class BloqueigCataleg implements AutoCloseable {

    private static String propietariActual = null;
    private static int profunditat = 0;
    private static int bloqueigsConcedits = 0;
    private static int comptadorMoments = 0;

    private final String idPropietari;
    private final int momentAdquisicio;
    private final boolean esReentrada;

    private boolean tancat = false;

    /**
     * Adquireix el bloqueig.
     *
     * @throws IllegalStateException si el te UN ALTRE propietari
     */
    public BloqueigCataleg(String idPropietari) {
        this.idPropietari = Objects.requireNonNull(idPropietari,
                "L'identificador del propietari no pot ser nul");

        if (propietariActual != null && !propietariActual.equals(idPropietari)) {
            throw new IllegalStateException(
                    "El cataleg esta bloquejat per " + propietariActual
                            + "; " + idPropietari + " no el pot adquirir");
        }

        this.esReentrada = (propietariActual != null);    // mateix propietari una altra vegada
        propietariActual = idPropietari;
        profunditat++;
        bloqueigsConcedits++;
        this.momentAdquisicio = ++comptadorMoments;

        System.out.println("    [bloqueig " + (esReentrada ? "REENTRANT " : "")
                + "adquirit per " + idPropietari + " (profunditat " + profunditat
                + ", moment " + momentAdquisicio + ")]");
    }

    /**
     * Allibera el bloqueig. Nomes l'allibera DE DEBO el mes extern.
     * Idempotent: tancar dues vegades no fa res la segona.
     */
    @Override
    public void close() {
        if (tancat) {
            return;                              // idempotent
        }
        tancat = true;
        profunditat--;

        if (profunditat == 0) {
            propietariActual = null;
            System.out.println("    [bloqueig ALLIBERAT per " + idPropietari + "]");
        } else {
            System.out.println("    [nivell reentrant tancat; profunditat " + profunditat + "]");
        }
    }

    /**
     * Comprova que qui opera es el propietari del bloqueig.
     *
     * @throws IllegalStateException si no ho es, o si no hi ha bloqueig
     */
    public static void comprovarPropietat(String id) {
        if (propietariActual == null) {
            throw new IllegalStateException(
                    "No hi ha cap bloqueig actiu sobre el cataleg; " + id + " no pot operar");
        }
        if (!propietariActual.equals(id)) {
            throw new IllegalStateException(
                    "El cataleg esta bloquejat per " + propietariActual + ", no per " + id);
        }
    }

    public String getIdPropietari()    { return idPropietari; }
    public int getMomentAdquisicio()   { return momentAdquisicio; }
    public boolean estaTancat()        { return tancat; }

    public static String getPropietariActual() { return propietariActual; }
    public static int getProfunditat()         { return profunditat; }
    public static int getBloqueigsConcedits()  { return bloqueigsConcedits; }

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

    public static void main(String[] args) {

        System.out.println("=== 1. Adquisicio i alliberament normal ===");
        try (BloqueigCataleg b = new BloqueigCataleg("EMP-001")) {
            System.out.println("  operant com a " + b.getIdPropietari());
            comprovarPropietat("EMP-001");
        }
        System.out.println("  Propietari despres de tancar: " + getPropietariActual());

        System.out.println("\n=== 2. Reentrancia de dos nivells ===");
        try (BloqueigCataleg extern = new BloqueigCataleg("EMP-001")) {
            System.out.println("  nivell extern");

            try (BloqueigCataleg intern = new BloqueigCataleg("EMP-001")) {
                System.out.println("  nivell intern");
                System.out.println("  Profunditat a dins: " + getProfunditat());
            }
            System.out.println("  Despres de tancar l'intern, profunditat: " + getProfunditat());
            System.out.println("  El propietari continua sent: " + getPropietariActual());
        }
        System.out.println("  Despres de tancar l'extern: " + getPropietariActual());

        System.out.println("\n=== 3. Un altre empleat intenta bloquejar ===");
        try (BloqueigCataleg b = new BloqueigCataleg("EMP-001")) {
            try (BloqueigCataleg altre = new BloqueigCataleg("EMP-002")) {
                System.out.println("  aixo no hauria de passar");
            } catch (IllegalStateException e) {
                System.out.println("  Rebutjat: " + e.getMessage());
            }
            System.out.println("  El bloqueig d'EMP-001 continua viu: " + getPropietariActual());
        }

        System.out.println("\n=== 4. Excepcio al cos ===");
        try (BloqueigCataleg b = new BloqueigCataleg("EMP-003")) {
            System.out.println("  operant...");
            throw new IllegalArgumentException("fallada al mig de l'operacio");
        } catch (IllegalArgumentException e) {
            System.out.println("  Excepcio rebuda INTACTA: " + e.getMessage());
        }
        System.out.println("  Propietari despres de la fallada: " + getPropietariActual()
                + " (alliberat correctament)");

        System.out.println("\n=== 5. close() doble ===");
        BloqueigCataleg manual = new BloqueigCataleg("EMP-001");
        manual.close();
        manual.close();
        System.out.println("  Sense errors. Profunditat: " + getProfunditat());

        System.out.println("\n=== 6. Operar sense bloqueig ===");
        try {
            comprovarPropietat("EMP-002");
        } catch (IllegalStateException e) {
            System.out.println("  " + e.getMessage());
        }

        System.out.println("\nBloqueigs concedits en total: " + getBloqueigsConcedits());
    }
}

Sortida:

=== 1. Adquisicio i alliberament normal ===
    [bloqueig adquirit per EMP-001 (profunditat 1, moment 1)]
  operant com a EMP-001
    [bloqueig ALLIBERAT per EMP-001]
  Propietari despres de tancar: null

=== 2. Reentrancia de dos nivells ===
    [bloqueig adquirit per EMP-001 (profunditat 1, moment 2)]
  nivell extern
    [bloqueig REENTRANT adquirit per EMP-001 (profunditat 2, moment 3)]
  nivell intern
  Profunditat a dins: 2
    [nivell reentrant tancat; profunditat 1]
  Despres de tancar l'intern, profunditat: 1
  El propietari continua sent: EMP-001
    [bloqueig ALLIBERAT per EMP-001]
  Despres de tancar l'extern: null

=== 3. Un altre empleat intenta bloquejar ===
    [bloqueig adquirit per EMP-001 (profunditat 1, moment 4)]
  Rebutjat: El cataleg esta bloquejat per EMP-001; EMP-002 no el pot adquirir
  El bloqueig d'EMP-001 continua viu: EMP-001
    [bloqueig ALLIBERAT per EMP-001]

=== 4. Excepcio al cos ===
    [bloqueig adquirit per EMP-003 (profunditat 1, moment 5)]
  operant...
    [bloqueig ALLIBERAT per EMP-003]
  Excepcio rebuda INTACTA: fallada al mig de l'operacio
  Propietari despres de la fallada: null (alliberat correctament)

=== 5. close() doble ===
    [bloqueig adquirit per EMP-001 (profunditat 1, moment 6)]
    [bloqueig ALLIBERAT per EMP-001]
  Sense errors. Profunditat: 0

=== 6. Operar sense bloqueig ===
  No hi ha cap bloqueig actiu sobre el cataleg; EMP-002 no pot operar

Bloqueigs concedits en total: 6

Fixa't en el cas 3: quan l'adquisició del segon bloqueig falla al constructor, el try-with-resources intern no arriba a tenir recurs que tancar, així que la profunditat de l'extern no es toca. Si el constructor hagués incrementat el comptador abans de validar, aquella fallada hauria deixat la profunditat descompensada per sempre. És el fail-fast de 06-03: validar abans de modificar.

Solució 2

package com.nexussoftware.bibliotech.presentacio;

/**
 * Explora el mecanisme d'excepcions suprimides amb tres recursos.
 */
public class CascadaDeFallades {

    static class RecursQueFalla implements AutoCloseable {
        private final String nom;
        private final boolean fallaEnUsar;
        private final boolean fallaEnTancar;

        RecursQueFalla(String nom, boolean fallaEnUsar, boolean fallaEnTancar) {
            this(nom, fallaEnUsar, fallaEnTancar, false);
        }

        RecursQueFalla(String nom, boolean fallaEnUsar, boolean fallaEnTancar,
                       boolean fallaEnObrir) {
            if (fallaEnObrir) {
                throw new IllegalStateException("OBERTURA fallida de " + nom);
            }
            this.nom = nom;
            this.fallaEnUsar = fallaEnUsar;
            this.fallaEnTancar = fallaEnTancar;
            System.out.println("    obrir  " + nom);
        }

        void usar() {
            System.out.println("    usar   " + nom);
            if (fallaEnUsar) {
                throw new IllegalStateException("US fallit de " + nom);
            }
        }

        @Override
        public void close() {
            System.out.println("    tancar " + nom);
            if (fallaEnTancar) {
                throw new IllegalStateException("TANCAMENT fallit de " + nom);
            }
        }
    }

    public static void main(String[] args) {

        escenari("1. No falla res", false, false, false, false, false, false);
        escenari("2. Falla nomes el cos", true, false, false, false, false, false);
        escenari("3. Falla nomes el tancament del central", false, false, false, false, true, false);
        escenari("4. Falla el cos I els tres tancaments", true, false, false, true, true, true);
        escenariOberturaFallida();
    }

    /**
     * @param fallaCos        si el cos llanca despres d'usar els recursos
     * @param tancament1/2/3  si falla el tancament de cada recurs
     */
    static void escenari(String titol, boolean fallaCos,
                         boolean usA, boolean usB,
                         boolean tancament1, boolean tancament2, boolean tancament3) {
        System.out.println("\n=== " + titol + " ===");
        try (RecursQueFalla r1 = new RecursQueFalla("R1", usA, tancament1);
             RecursQueFalla r2 = new RecursQueFalla("R2", usB, tancament2);
             RecursQueFalla r3 = new RecursQueFalla("R3", false, tancament3)) {

            r1.usar();
            r2.usar();
            r3.usar();

            if (fallaCos) {
                throw new IllegalStateException("COS fallit");
            }
            System.out.println("  (sense excepcio)");

        } catch (IllegalStateException e) {
            System.out.println("  --- Excepcio propagada ---");
            System.out.println(bolcar(e));
        }
    }

    static void escenariOberturaFallida() {
        System.out.println("\n=== 5. Falla l'OBERTURA del tercer recurs ===");
        try (RecursQueFalla r1 = new RecursQueFalla("R1", false, false);
             RecursQueFalla r2 = new RecursQueFalla("R2", false, false);
             RecursQueFalla r3 = new RecursQueFalla("R3", false, false, true)) {

            r1.usar();
            System.out.println("  aquesta linia NO s'executa");

        } catch (IllegalStateException e) {
            System.out.println("  --- Excepcio propagada ---");
            System.out.println(bolcar(e));
        }
    }

    /** Bolca l'arbre de causes i suprimides amb sagnat. */
    static String bolcar(Throwable t) {
        StringBuilder sb = new StringBuilder();
        bolcar(t, sb, 1, "");
        return sb.toString();
    }

    private static void bolcar(Throwable t, StringBuilder sb, int nivell, String etiqueta) {
        if (t == null || nivell > 10) { return; }

        sb.append("  ".repeat(nivell)).append(etiqueta)
          .append(t.getClass().getSimpleName()).append(": ")
          .append(t.getMessage()).append('\n');

        for (Throwable s : t.getSuppressed()) {
            bolcar(s, sb, nivell + 1, "Suppressed: ");
        }
        if (t.getCause() != null && t.getCause() != t) {
            bolcar(t.getCause(), sb, nivell + 1, "Caused by: ");
        }
    }
}

Sortida (abreujada) i anàlisi:

=== 1. No falla res ===
    obrir  R1
    obrir  R2
    obrir  R3
    usar   R1
    usar   R2
    usar   R3
  (sense excepcio)
    tancar R3
    tancar R2
    tancar R1

=== 2. Falla nomes el cos ===
    obrir  R1 / R2 / R3 ... usar R1 / R2 / R3
    tancar R3
    tancar R2
    tancar R1
  --- Excepcio propagada ---
    IllegalStateException: COS fallit

=== 3. Falla nomes el tancament del central ===
    ... tancar R3
    tancar R2
  --- Excepcio propagada ---
    IllegalStateException: TANCAMENT fallit de R2

=== 4. Falla el cos I els tres tancaments ===
    tancar R3
    tancar R2
    tancar R1
  --- Excepcio propagada ---
    IllegalStateException: COS fallit
      Suppressed: IllegalStateException: TANCAMENT fallit de R3
      Suppressed: IllegalStateException: TANCAMENT fallit de R2
      Suppressed: IllegalStateException: TANCAMENT fallit de R1

=== 5. Falla l'OBERTURA del tercer recurs ===
    obrir  R1
    obrir  R2
    tancar R2
    tancar R1
  --- Excepcio propagada ---
    IllegalStateException: OBERTURA fallida de R3

Anàlisi dels cinc escenaris:

# Es propaga Suprimides Recursos tancats
1 Res R3, R2, R1
2 La del cos 0 R3, R2, R1
3 La del tancament de R2 0 R3, R2 (i R1 s'intenta)
4 La del cos 3 R3, R2, R1
5 La de l'obertura de R3 0 R2, R1

Respostes a les preguntes:

  • Per què a l'escenari 4 es propaga la del cos? Perquè el mecanisme dona prioritat a la fallada del cos: és la que explica què ha anat malament. Les fallades de tancament són conseqüències secundàries, així que s'acumulen com a suprimides en comptes de substituir-la.
  • En quin ordre apareixen les suprimides? En l'ordre en què passen els tancaments, que és l'invers al d'obertura: R3, R2, R1. Cada close() que falla afegeix la seva excepció al final de l'array de la principal.
  • Escenari 3, per què no hi ha suprimides? Perquè no hi havia cap excepció prèvia quan va fallar el tancament de R2, així que aquella es converteix en la principal. I compte amb un detall: R1 també s'intenta tancar després —el mecanisme no avorta la cadena de tancaments—; si R1 també hagués fallat, la seva excepció hauria quedat suprimida sota la de R2.
  • Escenari 5, el més instructiu. En fallar el constructor de R3, R3 no existeix i no hi ha res a tancar-hi. Però R1 i R2 sí que es van obrir, i el mecanisme els tanca en ordre invers abans de propagar l'excepció d'obertura. El cos no s'executa mai. Aquest és precisament el cas que el patró manual de 06-05 feia malament tret que imbriquessis un try per recurs.

Solució 3

package com.nexussoftware.bibliotech.servei;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

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

/**
 * Importa materials des d'un fitxer de text al cataleg.
 *
 * NOTA SOBRE L'API D'E/S: aqui nomes es fan servir FileReader, BufferedReader i
 * BufferedWriter per obrir, llegir/escriure linies i tancar. L'entrada/sortida
 * completa (fluxos, NIO.2, formats) es el MODUL 7. El focus d'aquesta classe
 * esta en el TANCAMENT garantit amb try-with-resources.
 */
public class ImportadorCataleg {

    public record InformeImportacio(int llegides, int acceptades, int rebutjades,
                                    List<String> motius) {

        public String resum() {
            StringBuilder sb = new StringBuilder();
            sb.append("=== INFORME D'IMPORTACIO ===\n");
            sb.append("Linies llegides: ").append(llegides).append('\n');
            sb.append("Acceptades     : ").append(acceptades).append('\n');
            sb.append("Rebutjades     : ").append(rebutjades).append('\n');
            for (String m : motius) {
                sb.append("  * ").append(m).append('\n');
            }
            return sb.toString();
        }
    }

    private final Cataleg cataleg;

    public ImportadorCataleg(Cataleg cataleg) {
        this.cataleg = cataleg;
    }

    /**
     * Importa un fitxer al cataleg.
     *
     * NO captura la IOException d'obertura: la declara perque decideixi qui
     * crida si arrenca amb cataleg buit o avorta (06-03).
     */
    public InformeImportacio importar(String cami) throws IOException {
        List<String> motius = new ArrayList<>();
        int llegides = 0;
        int acceptades = 0;

        // try-with-resources: el lector es tanca sempre, fins i tot si
        // el bucle llanca. Amb try-finally manual caldrien 8 linies mes.
        try (BufferedReader lector = new BufferedReader(new FileReader(cami))) {

            String linia;
            while ((linia = lector.readLine()) != null) {
                llegides++;

                if (linia.isBlank() || linia.startsWith("#")) {
                    continue;                    // buides i comentaris: se salten sense soroll
                }

                // try DINS del bucle: una linia dolenta no avorta la importacio (06-02)
                try {
                    String[] c = linia.split(";");
                    Llibre llibre = new Llibre(c[0].trim(), c[1].trim(), c[2].trim(),
                            Integer.parseInt(c[3].trim()), c[4].trim());
                    cataleg.registrar(llibre);
                    acceptades++;

                } catch (ArrayIndexOutOfBoundsException e) {
                    motius.add("Linia " + llegides + ": falten camps -> '" + linia + "'");

                } catch (NumberFormatException e) {
                    motius.add("Linia " + llegides + ": any no numeric -> " + e.getMessage());

                } catch (ReferenciaDuplicadaException e) {
                    // Excepcio del DOMINI (06-04): usa les seves dades, no el seu missatge
                    motius.add("Linia " + llegides + ": " + e.getTipus() + " duplicat "
                            + e.getValorDuplicat() + ", ja la fa servir '"
                            + e.getTitolExistent() + "'");

                } catch (IllegalArgumentException e) {
                    // Validacions de Llibre (format de referencia, any, ISBN)
                    motius.add("Linia " + llegides + ": dada invalida -> " + e.getMessage());
                }
            }
        }
        // El lector ja esta tancat aqui, amb exit o amb fallada

        return new InformeImportacio(llegides, acceptades, llegides - acceptades, motius);
    }

    /**
     * Importa dins d'una sessio de treball: DOS recursos al mateix
     * try-with-resources, tancats en ordre invers al d'obertura.
     */
    public InformeImportacio importarAmbSessio(String cami, String idEmpleat, int dia)
            throws IOException {

        try (SessioBiblioteca sessio = new SessioBiblioteca(idEmpleat, dia, cataleg);
             BufferedReader lector = new BufferedReader(new FileReader(cami))) {

            List<String> motius = new ArrayList<>();
            int llegides = 0;
            int acceptades = 0;
            String linia;

            while ((linia = lector.readLine()) != null) {
                llegides++;
                if (linia.isBlank() || linia.startsWith("#")) { continue; }

                try {
                    String[] c = linia.split(";");
                    cataleg.registrar(new Llibre(c[0].trim(), c[1].trim(), c[2].trim(),
                            Integer.parseInt(c[3].trim()), c[4].trim()));
                    acceptades++;
                    sessio.anotarPrestec("ALTA " + c[0].trim());    // s'anota al torn

                } catch (RuntimeException e) {
                    motius.add("Linia " + llegides + ": " + e.getClass().getSimpleName()
                            + " - " + e.getMessage());
                }
            }
            return new InformeImportacio(llegides, acceptades, llegides - acceptades, motius);
        }
        // Tancament INVERS: primer el lector, despres la sessio (que consolida
        // les seves estadistiques i allibera el bloqueig del cataleg).
    }

    /** Crea un fitxer d'exemple si no existeix. Un altre try-with-resources. */
    public static void crearFitxerExemple(String cami) throws IOException {
        File fitxer = new File(cami);
        if (fitxer.exists()) {
            return;
        }
        try (BufferedWriter escriptor = new BufferedWriter(new FileWriter(cami))) {
            escriptor.write("# Cataleg d'exemple de BiblioTech");   escriptor.newLine();
            escriptor.write("# referencia;titol;autor;any;isbn");   escriptor.newLine();
            escriptor.write("LIB-0001;Java Eficac;Bloch;2018;978-0000000001");     escriptor.newLine();
            escriptor.write("LIB-0002;Patrons de Disseny;GoF;1994;978-0000000002"); escriptor.newLine();
            escriptor.write("LIB-0003;Refactoritzacio;Fowler;1999;978-0000000003"); escriptor.newLine();
            escriptor.write("LIB-0004;Sense autor");                                escriptor.newLine();
            escriptor.write("LIB-0005;Mal any;Autor;mil;978-0000000005");           escriptor.newLine();
            escriptor.write("LIB-0001;Duplicat;Altre;2020;978-0000000006");         escriptor.newLine();
            escriptor.write("LIB-0007;Any absurd;Autor;1200;978-0000000007");       escriptor.newLine();
        }
        System.out.println("  (fitxer d'exemple creat a " + cami + ")");
    }

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

    public static void main(String[] args) {
        String cami = "cataleg-entrada.txt";

        Cataleg cataleg = new Cataleg();
        ImportadorCataleg importador = new ImportadorCataleg(cataleg);

        System.out.println("=== 1. Preparacio ===");
        try {
            crearFitxerExemple(cami);
        } catch (IOException e) {
            System.out.println("  No s'ha pogut crear el fitxer d'exemple: " + e.getMessage());
            return;
        }

        System.out.println("\n=== 2. Importacio normal ===");
        try {
            InformeImportacio informe = importador.importar(cami);
            System.out.print(informe.resum());
            System.out.println("Cataleg: " + cataleg.mida() + " materials");
        } catch (IOException e) {
            System.out.println("  Fallada d'E/S: " + e.getMessage());
        }

        System.out.println("\n=== 3. Fitxer inexistent ===");
        try {
            importador.importar("no-existeix.txt");
        } catch (java.io.FileNotFoundException e) {
            // Subclasse d'IOException: es pot capturar per separat
            System.out.println("  El fitxer no existeix. Es continua amb el cataleg actual.");
            System.out.println("  Detall: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("  Una altra fallada d'E/S: " + e.getMessage());
        }

        System.out.println("\n=== 4. Importacio dins d'una sessio (dos recursos) ===");
        Cataleg altre = new Cataleg();
        ImportadorCataleg ambSessio = new ImportadorCataleg(altre);
        try {
            InformeImportacio informe = ambSessio.importarAmbSessio(cami, "EMP-001", 20);
            System.out.print(informe.resum());
        } catch (IOException e) {
            System.out.println("  Fallada: " + e.getMessage());
        }

        System.out.println("\n=== Estat final ===");
        System.out.println("  Cataleg principal: " + cataleg.mida() + " materials");
        System.out.println("  Torns completats: " + SessioBiblioteca.getTornsCompletats());
    }
}

Sortida:

=== 1. Preparacio ===
  (fitxer d'exemple creat a cataleg-entrada.txt)

=== 2. Importacio normal ===
=== INFORME D'IMPORTACIO ===
Linies llegides: 10
Acceptades     : 3
Rebutjades     : 7
  * Linia 6: falten camps -> 'LIB-0004;Sense autor'
  * Linia 7: any no numeric -> For input string: "mil"
  * Linia 8: REFERENCIA duplicat LIB-0001, ja la fa servir 'Java Eficac'
  * Linia 9: dada invalida -> L'any de publicacio ha d'estar entre 1450 i 2100, i era: 1200
Cataleg: 3 materials

=== 3. Fitxer inexistent ===
  El fitxer no existeix. Es continua amb el cataleg actual.
  Detall: no-existeix.txt (No such file or directory)

=== 4. Importacio dins d'una sessio (dos recursos) ===
  [sessio de EMP-001 oberta el dia 20]
  [sessio de EMP-001 tancada] 3 prestecs, 0 devolucions, 0,00 EUR
=== INFORME D'IMPORTACIO ===
Linies llegides: 10
Acceptades     : 3
Rebutjades     : 7
...

=== Estat final ===
  Cataleg principal: 3 materials
  Torns completats: 1

Tres punts que resumeix l'exercici:

  1. La IOException d'obertura es declara, no es captura. L'importador no sap si un fitxer absent ha d'avortar l'aplicació o no; aquella decisió és de main, que aquí opta per continuar. És la regla de 06-03 en acció.
  2. Dos nivells de try amb propòsits diferents. L'extern, amb recursos, garanteix el tancament. L'intern, dins del bucle, tolera línies defectuoses. Cap dels dos no podria fer la feina de l'altre.
  3. L'ordre de tancament invers és visible a la sortida del cas 4: el lector es tanca primer (silenciosament) i la sessió després, imprimint la seva consolidació abans que l'informe es mostri a main.

Conclusió

Ja domines la gestió automàtica de recursos. Saps que un try-with-resources declara els seus recursos entre parèntesis i que el compilador genera per tu el try-finally complet —amb la comprovació de null, el close() protegit i, sobretot, l'addSuppressed() que conserva l'excepció original—, de manera que les vint línies de comptabilitat manual del patró anterior a Java 7 es redueixen a zero i amb millor comportament.

Coneixes les dues interfícies: Closeable (java.io, close() throws IOException, idempotència exigida) per a recursos d'E/S, i AutoCloseable (java.lang, close() throws Exception) per a tota la resta; amb la regla pràctica de restringir el throws a la teva implementació, perquè cada excepció comprovada que declaris a close() l'imposes a tots els teus usuaris a cada bloc.

Has vist l'ordre de tancament invers al d'obertura demostrat amb traces, i per què és imprescindible quan un recurs n'embolcalla un altre. Saps que els recursos es tanquen abans del catch i del finally, que si falla l'obertura del segon recurs el primer es tanca igualment —el cas que el patró manual feia malament gairebé sempre—, i que la variable del recurs és implícitament final i no existeix fora del bloc, dues restriccions dirigides a impedir fuites i usos sobre recursos ja tancats. I coneixes la forma de Java 9 que admet una variable efectivament final ja existent, amb l'advertència de propietat que l'acompanya: tanca el que obres.

Entens el mecanisme de les excepcions suprimides, que és el que resol el problema que 06-05 va deixar obert: quan fallen el cos i el tancament, es propaga la del cos —la que explica què ha anat malament— i la del tancament queda accessible a getSuppressed(), amb diverses suprimides si fallen diversos tancaments, en l'ordre invers en què es van tancar. Saps distingir causa de suprimidaA va provocar B enfront de A i B van passar a més— i llegir un stack trace complet amb els seus blocs Caused by: i Suppressed: imbricats.

Saps quins recursos no s'han de tancar així: System.in el primer —d'aquí la convenció del mòdul 1 d'un sol Scanner compartit que mai no es tanca—, els recursos rebuts com a paràmetre, i els que el mètode retorna a qui crida, que es tancarien abans de retornar. I saps implementar AutoCloseable a les teves classes amb els seus quatre elements: marcador d'estat, close() idempotent, comprovació amb IllegalStateException a cada mètode d'ús i throws mínim. Amb les bones pràctiques del close(): idempotent, ràpid, que deixi l'objecte inutilitzable, que no llanci si es pot evitar —tret de quan el tancament és el moment en què les dades es confirmen, com un escriptor que buida la seva memòria intermèdia— i que mai no depengui de l'obsolet finalize().

BiblioTech ha guanyat dues classes tancables. SessioBiblioteca modela un torn de treball: adquireix el bloqueig del catàleg en construir-se i, en tancar-se, consolida les estadístiques del torn, allibera el bloqueig i queda inutilitzable, se surti del bloc com se surti. La demostració ho prova: quan el torn de Diego falla a mitges per una multa negativa, la sessió es tanca igualment, les estadístiques es consoliden amb el que sí que es va fer, el bloqueig s'allibera i l'excepció arriba al catch intacta, sense una sola línia de finally escrita a mà. I ExportadorCataleg implementa Closeable amb un close() que que propaga la seva IOException, perquè en tancar es buida la memòria intermèdia al disc i aquella fallada significa que les dades no s'han guardat.

Amb això, les quatre primeres fragilitats del mòdul 5 estan resoltes: les dades invàlides ja no avorten el programa, els errors no es comuniquen amb null ni false muts, les operacions no deixen l'estat a mitges i els recursos no es queden oberts. Queda una cosa d'aquest mòdul, i és la que converteix tot l'anterior en programari de producció: decidir on es captura cada error i deixar d'escriure avisos amb System.out.println.

Això és la lliçó següent i última del mòdul, Estratègies de gestió d'errors i logging: on capturar —la regla de capturar on es pot decidir, no on es produeix—, què fa cada capa de BiblioTech amb els errors, la frontera d'errors del main i el gestor global amb Thread.setDefaultUncaughtExceptionHandler; quina informació ha d'arribar a l'usuari i quina només al registre —mai un stack trace a la cara de ningú—; errors recuperables enfront d'irrecuperables, degradació elegant, reintents amb límit, validació prèvia enfront d'excepció, i el patró "objecte resultat". I tota la part de logging: per què System.out.println no serveix en producció, la taula de nivells i què registrar a cadascun, java.util.logging a la pràctica —Logger per classe, Handler de consola i de fitxer, Formatter, configuració per logging.properties i el registre d'una excepció amb log(Level.SEVERE, msg, e) en comptes de printStackTrace()—, què no registrar mai, com s'escriu un missatge de log útil, i l'ecosistema real amb SLF4J i Logback que veuràs a 11-07.

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