Queden dues fragilitats de la llista que va tancar el mòdul 5, i aquesta lliçó ataca la més traïdora: les operacions que fallen a mitges i deixen l'estat incoherent. Si GestorPrestecs.prestar marca el material com a prestat, incrementa el comptador de l'empleat i llavors falla en anotar-lo al registre, BiblioTech queda amb un material bloquejat que ningú no té prestat i un empleat amb un préstec fantasma que li consumeix quota. Cap excepció, per ben dissenyada que estigui, no arregla això: cal un mecanisme que garanteixi que cert codi s'executa passi el que passi.
Aquest mecanisme és finally. És la construcció més simple del mòdul —un bloc que sempre s'executa— i alhora la que amaga més paranys: el return dins de finally que descarta silenciosament el valor i fins i tot l'excepció pendent, el finally que llança i fa desaparèixer la fallada original, i els dos casos en què ni tan sols finally s'executa.
Al final veuràs el patró manual de tancament de recursos anterior a Java 7 en tota la seva verbositat. No és nostàlgia: és l'única manera d'entendre de debò què fa per tu el try-with-resources de la lliçó següent, i per què la seva existència estava tan justificada.
Contingut
- Què garanteix
finally - L'ordre exacte d'execució
finallyambreturn,breakicontinue- Les combinacions vàlides
try-finallysensecatch- El propòsit clàssic: alliberar recursos
- El parany del
returnafinally - El
finallyque llança i perd l'excepció original - Els dos casos en què
finallyNO s'executa - El patró manual de tancament abans de Java 7
- BiblioTech: operacions que no deixen l'estat a mitges
- Errors Comuns i Consells
- Exercicis
- Què garanteix
finally
finallyfinally és un bloc opcional que s'afegeix a un try, i la seva garantia és aquesta:
El bloc
finallys'executa sempre que s'hagi entrat altry, hi hagi o no excepció, es capturi o no, i amb independència de com se surti del bloc.
Les quatre sortides possibles d'un try, i què fa finally en cadascuna:
Com acaba el try |
S'executa finally? |
|---|---|
| Acaba normalment | Sí |
Llança una excepció capturada per un catch |
Sí, després del catch |
| Llança una excepció no capturada | Sí, abans que l'excepció continuï pujant |
Executa return, break o continue |
Sí, abans de saltar |
Un primer exemple que mostra els tres primers casos:
package com.nexussoftware.bibliotech.presentacio;
public class GarantiaDeFinally {
public static void main(String[] args) {
System.out.println("--- CAS 1: sense excepcio ---");
processar("15");
System.out.println("\n--- CAS 2: excepcio capturada ---");
processar("dotze");
System.out.println("\n--- CAS 3: excepcio NO capturada ---");
try {
processarSenseXarxa(null);
} catch (NullPointerException e) {
System.out.println(" (capturada a dalt: " + e.getClass().getSimpleName() + ")");
}
}
static void processar(String entrada) {
System.out.println(" 1. abans del try");
try {
System.out.println(" 2. dins del try");
int dies = Integer.parseInt(entrada);
System.out.println(" 3. convertit: " + dies);
} catch (NumberFormatException e) {
System.out.println(" 4. al catch: " + e.getMessage());
} finally {
System.out.println(" 5. AL FINALLY");
}
System.out.println(" 6. despres del bloc");
}
static void processarSenseXarxa(String entrada) {
try {
System.out.println(" 2. dins del try");
System.out.println(" 3. longitud: " + entrada.length()); // NullPointerException
} catch (NumberFormatException e) {
System.out.println(" 4. aquest catch NO captura NullPointerException");
} finally {
System.out.println(" 5. AL FINALLY (encara que ningu no hagi capturat)");
}
System.out.println(" 6. aquesta linia NO s'executa");
}
}Sortida:
--- CAS 1: sense excepcio --- 1. abans del try 2. dins del try 3. convertit: 15 5. AL FINALLY 6. despres del bloc --- CAS 2: excepcio capturada --- 1. abans del try 2. dins del try 4. al catch: For input string: "dotze" 5. AL FINALLY 6. despres del bloc --- CAS 3: excepcio NO capturada --- 2. dins del try 5. AL FINALLY (encara que ningu no hagi capturat) (capturada a dalt: NullPointerException)
El cas 3 és el revelador. Cap catch local no era compatible amb NullPointerException, així que l'excepció va continuar pujant... però el finally es va executar igualment, abans que se n'anés. La línia 6 no es va executar, perquè el mètode va ser abandonat. Aquesta combinació —"la resta del mètode s'abandona, però el finally s'executa"— és exactament el que fa útil el bloc per alliberar recursos i per reparar estat.
- L'ordre exacte d'execució
Quan hi ha excepció, l'ordre és: try fins al punt de fallada → catch compatible (si n'hi ha) → finally → el que vingui després.
flowchart TB
A["Entra al try"] --> B{"Llanca excepcio?"}
B -->|"no"| C["El try acaba"]
C --> F["FINALLY"]
F --> G["Continua despres del bloc"]
B -->|"si"| D{"Hi ha catch compatible?"}
D -->|"si"| E["Executa el catch"]
E --> F2["FINALLY"]
F2 --> G
D -->|"no"| H["FINALLY"]
H --> I["L'excepcio segueix pujant<br/>per la pila de crides"]
Amb try imbricats, cada finally s'executa de dins cap a fora a mesura que l'excepció va pujant:
package com.nexussoftware.bibliotech.presentacio;
public class OrdreDeFinally {
public static void main(String[] args) {
try {
nivell1();
} catch (IllegalStateException e) {
System.out.println("6. main captura: " + e.getMessage());
}
}
static void nivell1() {
try {
System.out.println("1. nivell1: try");
nivell2();
} finally {
System.out.println("5. nivell1: FINALLY");
}
}
static void nivell2() {
try {
System.out.println("2. nivell2: try");
nivell3();
} finally {
System.out.println("4. nivell2: FINALLY");
}
}
static void nivell3() {
System.out.println("3. nivell3: llanca");
throw new IllegalStateException("fallada al nivell mes profund");
}
}Sortida:
1. nivell1: try 2. nivell2: try 3. nivell3: llanca 4. nivell2: FINALLY 5. nivell1: FINALLY 6. main captura: fallada al nivell mes profund
Els finally es disparen en ordre invers a la profunditat, exactament igual que es desapilen els marcs. És la mateixa pila de crides de 05-08 i de 06-01: mentre l'excepció desenrotlla la pila, cada marc que es descarta executa el seu finally abans de desaparèixer. Aquesta és la propietat que permet que cada nivell netegi el que és seu sense saber res dels altres.
finally amb return, break i continue
finally amb return, break i continueAquí hi ha la part que sorprèn: finally s'executa fins i tot quan el try o el catch fan return. La JVM guarda el valor de retorn, executa el finally, i només llavors retorna.
package com.nexussoftware.bibliotech.presentacio;
public class FinallyAmbReturn {
public static void main(String[] args) {
System.out.println("Resultat A: " + ambReturnAlTry());
System.out.println("Resultat B: " + ambReturnAlCatch());
System.out.println("\n--- break i continue ---");
ambBreak();
ambContinue();
}
/** El return del try es "congela": el finally s'executa ABANS de retornar. */
static int ambReturnAlTry() {
try {
System.out.println(" [A] al try, fare return 10");
return 10;
} finally {
System.out.println(" [A] FINALLY (s'executa abans de retornar el 10)");
}
}
static int ambReturnAlCatch() {
try {
System.out.println(" [B] al try, llanco");
throw new IllegalStateException("fallada");
} catch (IllegalStateException e) {
System.out.println(" [B] al catch, fare return 20");
return 20;
} finally {
System.out.println(" [B] FINALLY (tambe despres del return del catch)");
}
}
/** Amb break: el finally s'executa abans de sortir del bucle. */
static void ambBreak() {
for (int i = 1; i <= 3; i++) {
try {
System.out.println(" [break] volta " + i);
if (i == 2) {
break;
}
} finally {
System.out.println(" [break] FINALLY de la volta " + i);
}
}
System.out.println(" [break] fora del bucle");
}
/** Amb continue: el finally s'executa abans de passar a la volta seguent. */
static void ambContinue() {
for (int i = 1; i <= 3; i++) {
try {
if (i == 2) {
System.out.println(" [continue] salto la volta 2");
continue;
}
System.out.println(" [continue] processo la volta " + i);
} finally {
System.out.println(" [continue] FINALLY de la volta " + i);
}
}
}
}Sortida:
[A] al try, fare return 10 [A] FINALLY (s'executa abans de retornar el 10) Resultat A: 10 [B] al try, llanco [B] al catch, fare return 20 [B] FINALLY (tambe despres del return del catch) Resultat B: 20 --- break i continue --- [break] volta 1 [break] FINALLY de la volta 1 [break] volta 2 [break] FINALLY de la volta 2 [break] fora del bucle [continue] processo la volta 1 [continue] FINALLY de la volta 1 [continue] salto la volta 2 [continue] FINALLY de la volta 2 [continue] processo la volta 3 [continue] FINALLY de la volta 3
La mecànica exacta amb el return, que convé entendre bé perquè és la base del parany de l'apartat 7:
- S'avalua l'expressió del
return. En el cas A, el valor10es calcula i es guarda. - S'executa el
finally. - Es retorna el valor guardat al pas 1.
La conseqüència és subtil però important: si el finally modifica la variable que s'anava a retornar, el canvi no afecta el valor retornat, perquè el valor ja s'ha copiat.
static int demostracio() {
int valor = 10;
try {
return valor; // es guarda el 10 ARA
} finally {
valor = 99; // modifica la variable, no el valor guardat
System.out.println(" finally ha posat valor = " + valor);
}
}
// Imprimeix "finally ha posat valor = 99" i RETORNA 10Compte amb l'excepció a aquesta regla: si el que retornes és una referència a un objecte mutable, el finally sí que pot modificar l'objecte apuntat, perquè el que s'ha copiat ha estat la referència, no el contingut.
static List<String> demostracioObjecte() {
List<String> llista = new ArrayList<>(List.of("a"));
try {
return llista; // es guarda la REFERENCIA
} finally {
llista.add("b"); // modifica l'OBJECTE apuntat: si que afecta
}
}
// Retorna [a, b]És el mateix pas per valor de referències que vas veure a 03-03, aplicat aquí.
- Les combinacions vàlides
Un try admet tres formes legals:
| Forma | Vàlida? | Per a què serveix |
|---|---|---|
try + catch |
Sí | Gestionar la fallada |
try + catch + finally |
Sí | Gestionar la fallada i netejar |
try + finally |
Sí | Netejar sense gestionar: la fallada continua pujant |
try sol |
No compila | — |
try + finally + catch (en aquest ordre) |
No compila | El finally sempre va l'últim |
// error: 'try' without 'catch', 'finally' or resource declarations
try {
ferAlgunaCosa();
}
// error: 'catch' without 'try' (el finally ha d'anar al final)
try {
ferAlgunaCosa();
} finally {
netejar();
} catch (Exception e) { // NO COMPILA
gestionar(e);
}També hi pot haver diversos catch i un sol finally, sempre al final:
try {
operacio();
} catch (MaterialNoTrobatException e) {
// ...
} catch (PrestecException e) {
// ...
} finally {
alliberar(); // un de sol, i l'ultim
}
try-finally sense catch
try-finally sense catchLa forma menys coneguda i una de les més útils. Significa exactament: "no sé gestionar aquesta fallada, però he de netejar abans que se'n vagi".
package com.nexussoftware.bibliotech.servei;
public class TryFinallySenseCatch {
private boolean catalegBloquejat = false;
/**
* Reorganitza el cataleg sota bloqueig.
*
* No captura RES: si la reorganitzacio falla, la fallada ha de pujar perque
* a dalt decideixin. Pero el bloqueig cal alliberar-lo SEMPRE, o el
* cataleg quedaria inaccessible per a la resta de l'aplicacio.
*/
public void reorganitzar(int criteri) {
bloquejar();
try {
System.out.println(" Reorganitzant amb criteri " + criteri);
if (criteri < 0) {
throw new IllegalArgumentException("Criteri invalid: " + criteri);
}
System.out.println(" Reorganitzacio completada");
} finally {
desbloquejar(); // s'executa amb exit I amb fallada
}
}
private void bloquejar() {
catalegBloquejat = true;
System.out.println(" [bloqueig adquirit]");
}
private void desbloquejar() {
catalegBloquejat = false;
System.out.println(" [bloqueig alliberat]");
}
public boolean estaBloquejat() { return catalegBloquejat; }
public static void main(String[] args) {
TryFinallySenseCatch servei = new TryFinallySenseCatch();
System.out.println("Cas correcte:");
servei.reorganitzar(1);
System.out.println("Bloquejat despres de l'exit? " + servei.estaBloquejat());
System.out.println("\nCas amb fallada:");
try {
servei.reorganitzar(-1);
} catch (IllegalArgumentException e) {
System.out.println(" main captura: " + e.getMessage());
}
System.out.println("Bloquejat despres de la fallada? " + servei.estaBloquejat());
}
}Sortida:
Cas correcte: [bloqueig adquirit] Reorganitzant amb criteri 1 Reorganitzacio completada [bloqueig alliberat] Bloquejat despres de l'exit? false Cas amb fallada: [bloqueig adquirit] Reorganitzant amb criteri -1 [bloqueig alliberat] main captura: Criteri invalid: -1 Bloquejat despres de la fallada? false
El que és important: l'excepció va arribar a main intacta, amb el seu tipus, el seu missatge i la seva pila, i tanmateix el bloqueig es va alliberar. Això és el que fa try-finally insubstituïble: separa completament "netejar" de "gestionar", que són responsabilitats diferents i que sovint corresponen a capes diferents.
Sense finally, l'única alternativa seria duplicar el desbloquejar() al camí normal i en un catch que rellancés, amb el risc evident d'oblidar-ne un dels dos:
// MALAMENT: fragil i duplicat
bloquejar();
try {
reorganitzarIntern(criteri);
desbloquejar(); // cami normal
} catch (RuntimeException e) {
desbloquejar(); // cami de fallada... i si algu hi afegeix un altre return?
throw e;
}
- El propòsit clàssic: alliberar recursos
L'ús històric de finally és tancar el que s'ha obert: un fitxer, una connexió, un bloqueig, un socket.
Avís d'abast: els exemples següents fan servir
FileReaderiBufferedReaderde manera deliberadament mínima. L'entrada/sortida de fitxers és el mòdul 7 —lectura, escriptura, fluxos, NIO.2— i allà s'explica l'API a fons. Aquí l'única cosa que importa és el tancament: què passa si no tanques, i com garantir que es tanca.
Per què cal tancar. Un FileReader obert consumeix un descriptor de fitxer, un recurs del sistema operatiu que és limitat (a Linux, típicament 1024 per procés si no es canvia el límit). Si obres fitxers en un bucle i no els tanques, el procés esgota els descriptors i tot comença a fallar amb Too many open files. I en escriptura és pitjor: les dades poden quedar-se a la memòria intermèdia sense arribar al disc.
L'intent ingenu, que està malament:
// MALAMENT: si readLine llanca IOException, el fitxer NO es tanca MAI
BufferedReader lector = new BufferedReader(new FileReader("cataleg.txt"));
String linia = lector.readLine();
processar(linia); // si aixo llanca, ens saltem el close
lector.close(); // aquesta linia pot no executar-se maiLa versió amb finally:
package com.nexussoftware.bibliotech.servei;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
/**
* Lectura d'un fitxer amb tancament garantit per finally.
*
* NOTA: l'API d'E/S s'estudia al modul 7. Aqui nomes interessa el TANCAMENT.
*/
public class LectorAmbFinally {
public int comptarLinies(String cami) throws IOException {
BufferedReader lector = null; // declarada FORA per veure-la al finally
int linies = 0;
try {
lector = new BufferedReader(new FileReader(cami));
String linia;
while ((linia = lector.readLine()) != null) {
if (!linia.isBlank()) {
linies++;
}
}
return linies;
} finally {
// S'executa amb exit i amb fallada
if (lector != null) { // pot haver fallat el mateix constructor
lector.close(); // close() declara IOException: veure apartat 10
}
System.out.println(" [fitxer tancat]");
}
}
}Tres detalls que ja apunten aquí i que es desenvolupen a l'apartat 10:
- La variable es declara fora del
try, perquè elfinallyl'ha de veure (abast de blocs, 06-02). - Cal comprovar
!= null, perquè si el constructor deFileReaderfalla —fitxer inexistent—,lectorcontinua valentnulli elclose()llançaria unNullPointerException. - El mateix
close()declaraIOException, així que en molts casos cal un altretrydins delfinally. Aquí és on la cosa es torna lletja.
- El parany del
return a finally
return a finallyAquest és el parany que cal conèixer per no caure-hi mai.
Un
returndins definallydescarta el valor de retorn deltry, i —molt pitjor— descarta també qualsevol excepció pendent.
package com.nexussoftware.bibliotech.presentacio;
public class ParanyDelReturnAlFinally {
public static void main(String[] args) {
System.out.println("A) " + descartaElValor());
System.out.println("B) " + descartaExcepcio());
System.out.println("C) " + versioCorrecta());
}
/** El return del finally GUANYA: el 10 es perd. */
static int descartaElValor() {
try {
return 10;
} finally {
return 99; // el compilador avisa, pero compila
}
}
/**
* MOLT PITJOR: el return del finally s'EMPASSA l'excepcio.
* El metode retorna -1 com si tot hagues anat be.
*/
static int descartaExcepcio() {
try {
throw new IllegalStateException("El cataleg esta corrupte");
} finally {
return -1; // l'excepcio DESAPAREIX. Sense rastre.
}
}
/** Com s'ha de fer: el finally nomes neteja, no decideix. */
static int versioCorrecta() {
try {
return 10;
} finally {
System.out.println(" (neteja, sense return)");
}
}
}Sortida:
Atura't al cas B. El mètode va llançar un IllegalStateException dient que el catàleg està corrupte, i qui crida va rebre tranquil·lament un -1. L'excepció no va pujar, no es va registrar, no va deixar cap rastre. És un catch buit camuflat, amb l'agreujant que ni tan sols hi ha un catch a la vista que et posi sobre avís en llegir el codi.
El mateix passa amb break, continue i amb un throw dins del finally: qualsevol sortida abrupta des del finally descarta el que estigués en curs.
for (String referencia : referencies) {
try {
processar(referencia);
throw new IllegalStateException("fallada processant " + referencia);
} finally {
continue; // s'empassa TOTES les excepcions del bucle
}
}Els compiladors moderns i tots els analitzadors estàtics (SpotBugs, SonarQube, els mateixos avisos de l'IDE) marquen això com a error greu. L'avís de javac amb -Xlint:finally és:
La regla, sense matisos:
No posis mai
return,break,continuenithrowdins d'unfinally. Elfinallyneteja; no decideix, no retorna i no llança.
- El
finally que llança i perd l'excepció original
finally que llança i perd l'excepció originalUna variant del problema anterior que apareix sense voler, i que és la que motiva directament la lliçó següent.
Quan el try llança una excepció i el finally en llança una altra, la del finally guanya i la del try es perd completament. I això passa en el cas més comú de tots: tancar un recurs.
package com.nexussoftware.bibliotech.presentacio;
public class FinallyQuePerdExcepcio {
/** Simula un recurs el tancament del qual tambe pot fallar. */
static class RecursFragil {
private final String nom;
RecursFragil(String nom) {
this.nom = nom;
System.out.println(" [obert " + nom + "]");
}
void llegir() {
throw new IllegalStateException("FALLADA REAL: el fitxer " + nom + " esta corrupte");
}
void tancar() {
throw new IllegalStateException("FALLADA EN TANCAR: no s'ha pogut alliberar " + nom);
}
}
public static void main(String[] args) {
try {
operar();
} catch (IllegalStateException e) {
System.out.println("\nExcepcio rebuda a main:");
System.out.println(" " + e.getMessage());
System.out.println(" Causa: " + e.getCause());
System.out.println(" Suprimides: " + e.getSuppressed().length);
}
}
static void operar() {
RecursFragil recurs = new RecursFragil("cataleg.txt");
try {
recurs.llegir(); // llanca la FALLADA REAL
} finally {
recurs.tancar(); // llanca UNA ALTRA, i la primera desapareix
}
}
}Sortida:
[obert cataleg.txt] Excepcio rebuda a main: FALLADA EN TANCAR: no s'ha pogut alliberar cataleg.txt Causa: null Suprimides: 0
La fallada real ha desaparegut. El fitxer estava corrupte —això és el que cal arreglar— però l'única cosa que arriba a dalt és un missatge sobre el tancament, que és un símptoma secundari. I no en queda ni rastre: getCause() és null i getSuppressed() està buit.
És un problema greu i sorprenentment freqüent, perquè el close() de gairebé qualsevol recurs real pot llançar: un BufferedWriter que buida la seva memòria intermèdia en tancar, una connexió de xarxa que ja s'havia caigut, un bloqueig que un altre fil ha alliberat.
El pedaç manual existeix, i és horrible:
static void operarAmbPedac() {
RecursFragil recurs = new RecursFragil("cataleg.txt");
IllegalStateException falladaPrincipal = null;
try {
recurs.llegir();
} catch (IllegalStateException e) {
falladaPrincipal = e; // guardem l'original
throw e;
} finally {
try {
recurs.tancar();
} catch (IllegalStateException eTancament) {
if (falladaPrincipal != null) {
falladaPrincipal.addSuppressed(eTancament); // l'afegim com a suprimida
} else {
throw eTancament; // no hi havia original: aquesta es l'unica
}
}
}
}Tretze línies de comptabilitat manual per a una operació de dues. I cal repetir-les a cada recurs, a cada mètode. Ningú no ho fa bé de manera consistent.
Aquesta és exactament la raó per la qual existeix
try-with-resources, la construcció de la lliçó següent. Fa automàticament tot l'anterior: conserva l'excepció original, registra la del tancament com a suprimida —recuperable ambgetSuppressed()— i no t'obliga a escriure ni una línia d'aquella comptabilitat.
- Els dos casos en què
finally NO s'executa
finally NO s'executaLa garantia de finally és forta, però no absoluta. Hi ha exactament dues situacions en què no s'executa, i totes dues tenen alguna cosa en comú: la JVM deixa d'existir o el fil deixa d'executar.
Cas 1: System.exit().
package com.nexussoftware.bibliotech.presentacio;
public class FinallyISystemExit {
public static void main(String[] args) {
// Un hook d'aturada SI que s'executa amb System.exit
Runtime.getRuntime().addShutdownHook(new Thread(() ->
System.out.println("HOOK D'ATURADA: aixo si que s'executa")));
try {
System.out.println("1. al try");
System.exit(0); // la JVM acaba AQUI MATEIX
System.out.println("2. inabastable");
} finally {
System.out.println("3. AQUEST FINALLY NO S'EXECUTA");
}
}
}Sortida:
System.exit() no llança cap excepció ni retorna: demana a la JVM que acabi immediatament. No hi ha desenrotllament de pila, així que no hi ha finally que executar.
El que sí que s'executa són els hooks d'aturada, fils registrats amb Runtime.getRuntime().addShutdownHook(...) que la JVM arrenca abans de morir. Són el mecanisme correcte per a la neteja global d'una aplicació: tancar el pool de connexions, buidar les memòries intermèdies de log, guardar l'estat. A BiblioTech, serà el lloc natural per persistir el catàleg quan arribi el mòdul 7. Dues advertències: els hooks tampoc no s'executen davant d'un Runtime.halt() o un senyal SIGKILL, i han de ser ràpids, perquè molts entorns maten el procés si triguen.
Cas 2: la JVM o el fil acaben de manera anòmala.
- La JVM cau per una fallada greu (
kill -9, tall de corrent, error del mateix procés natiu). - Un
Errordels que deixen la JVM inutilitzable. UnStackOverflowErroren unfinallyque necessita pila per executar-se, o unOutOfMemoryErrorquan el mateixfinallynecessita reservar memòria. - El fil s'atura amb l'obsolet
Thread.stop()(eliminat a les versions modernes de Java).
D'aquí es deriva una conclusió pràctica important:
finallygaranteix la coherència dins del procés, no la durabilitat fora d'ell. Si necessites que alguna cosa sobrevisqui a una caiguda de la JVM,finallyno n'hi ha prou: necessites persistència (mòdul 7) o transaccions (mòdul 11).
- El patró manual de tancament abans de Java 7
Recopilem tot l'anterior en el patró complet i correcte de gestió de recursos tal com calia escriure'l abans del 2011. Prepara't.
package com.nexussoftware.bibliotech.servei;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Patro manual de tancament de recursos, anterior a Java 7.
*
* S'inclou perque vegis exactament quina feina fa per tu el
* try-with-resources de la llico 06-06. En codi nou, NO S'ESCRIU AIXI.
*
* (L'API d'E/S es materia del modul 7: aqui nomes interessa el tancament.)
*/
public class LectorManualPreJava7 {
/** UN sol recurs. Ja es incomode. */
public List<String> llegirReferencies(String cami) throws IOException {
List<String> referencies = new ArrayList<>();
BufferedReader lector = null; // 1. declarar FORA
try {
lector = new BufferedReader(new FileReader(cami));
String linia;
while ((linia = lector.readLine()) != null) {
if (!linia.isBlank()) {
referencies.add(linia.split(";")[0].trim());
}
}
return referencies;
} finally {
if (lector != null) { // 2. comprovar null
try {
lector.close(); // 3. close() llanca IOException
} catch (IOException eTancament) {
// 4. que en fem, d'aixo? Si el rellancem, PERDEM
// l'excepcio original del try (apartat 8).
// L'unic raonable sense ajuda del llenguatge es registrar-ho.
System.err.println("Avis: no s'ha pogut tancar " + cami
+ ": " + eTancament.getMessage());
}
}
}
}
/** DOS recursos. Aqui comenca el desastre. */
public void copiarCataleg(String origen, String desti) throws IOException {
BufferedReader lector = null;
java.io.BufferedWriter escriptor = null;
try {
lector = new BufferedReader(new FileReader(origen));
escriptor = new java.io.BufferedWriter(new java.io.FileWriter(desti));
String linia;
while ((linia = lector.readLine()) != null) {
escriptor.write(linia);
escriptor.newLine();
}
} finally {
// Ordre INVERS al d'obertura: primer l'escriptor, despres el lector.
// I cada tancament necessita el seu propi try, perque si el primer llanca,
// el segon NO s'executaria.
try {
if (escriptor != null) { escriptor.close(); }
} catch (IOException e) {
System.err.println("Avis: fallada en tancar el desti: " + e.getMessage());
} finally {
try {
if (lector != null) { lector.close(); }
} catch (IOException e) {
System.err.println("Avis: fallada en tancar l'origen: " + e.getMessage());
}
}
}
}
}Compta el que cal recordar per escriure això bé:
| # | Requisit | Què passa si s'oblida |
|---|---|---|
| 1 | Declarar la variable fora del try |
No compila: el finally no la veu |
| 2 | Inicialitzar-la a null |
No compila: variable possiblement no inicialitzada |
| 3 | Comprovar != null al finally |
NullPointerException si ha fallat l'obertura |
| 4 | Embolcallar el close() al seu propi try |
No compila si close() declara comprovada |
| 5 | No rellançar des del catch del tancament |
Es perd l'excepció original (apartat 8) |
| 6 | Tancar en ordre invers al d'obertura | El recurs dependent es tanca sobre un de ja tancat |
| 7 | Un try/finally imbricat per cada recurs addicional |
Si el primer tancament falla, els altres no es tanquen |
Amb dos recursos són vint línies de comptabilitat per a quatre línies de feina real. Amb tres, és immantenible. El resultat previsible: durant anys, una fracció enorme del codi Java en producció tenia fuites de descriptors, perquè gairebé ningú no escrivia els set punts correctament.
Java 7 va resoldre això d'una revolada. Aquest és el mateix copiarCataleg amb try-with-resources, perquè vegis cap a on vas:
public void copiarCataleg(String origen, String desti) throws IOException {
try (BufferedReader lector = new BufferedReader(new FileReader(origen));
BufferedWriter escriptor = new BufferedWriter(new FileWriter(desti))) {
String linia;
while ((linia = lector.readLine()) != null) {
escriptor.write(linia);
escriptor.newLine();
}
}
// Tancament automatic, en ordre invers, amb les excepcions de tancament
// registrades com a SUPRIMIDES sense perdre l'original.
}Vint línies convertides en zero. Això és la lliçó següent.
- BiblioTech: operacions que no deixen l'estat a mitges
Ara l'aplicació que resol la quarta fragilitat del mòdul 5. El problema, en concret:
// VERSIO FRAGIL: si alguna cosa falla al mig, l'estat queda incoherent
public Prestec prestar(String referencia, String idEmpleat, int dia) {
Material material = cataleg.obtenirPerReferencia(referencia);
Empleat empleat = obtenirEmpleat(idEmpleat);
material.prestar(); // pas 1: el material queda BLOQUEJAT
empleat.registrarPrestec(); // pas 2: l'empleat gasta QUOTA
Prestec p = new Prestec(seguentReferencia(), material, empleat, dia);
registre.anotar(p); // pas 3: si AIXO falla...
cuaReserves.notificarPrestec(referencia);
return p;
}Si el pas 3 llança, el material queda marcat com a prestat sense cap préstec que ho justifiqui i l'empleat perd un forat de la seva quota per sempre. El material és inaccessible: ningú no el pot prestar perquè figura ocupat, i ningú no el pot retornar perquè no existeix el préstec.
La solució amb finally: un indicador d'èxit i un bloc de compensació.
package com.nexussoftware.bibliotech.servei;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import com.nexussoftware.bibliotech.domini.*;
/**
* Orquestra prestecs garantint que, si l'operacio falla a mitges,
* l'estat del Cataleg i del RegistrePrestecs queda COHERENT.
*
* Tecnica: marcador d'exit + bloc de compensacio al finally.
* Es la versio manual del concepte de TRANSACCIO; la de debo,
* amb base de dades, arriba al modul 11.
*/
public class GestorPrestecs {
private final Cataleg cataleg;
private final Map<String, Empleat> empleats = new HashMap<>();
private final Map<String, Prestec> prestecs = new HashMap<>();
private int comptador = 0;
public GestorPrestecs(Cataleg cataleg) {
this.cataleg = Objects.requireNonNull(cataleg, "El cataleg no pot ser nul");
}
public void donarAlta(Empleat empleat) {
Objects.requireNonNull(empleat, "L'empleat no pot ser nul");
empleats.put(empleat.getIdentificador(), empleat);
}
/**
* Presta un material deixant el sistema coherent passi el que passi.
*
* Els passos que MODIFIQUEN estat es marquen amb marcadors. Si en arribar al
* finally l'operacio no s'ha completat, es desfan en ordre invers.
*/
public Prestec prestar(String referencia, String idEmpleat, int dia) {
Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
Objects.requireNonNull(idEmpleat, "L'identificador no pot ser nul");
if (dia < 1) {
throw new IllegalArgumentException("El dia ha de ser 1 o posterior, i era: " + dia);
}
// --- Fase 1: LECTURA. No modifica res, aixi que una fallada aqui es inocua ---
Material material = cataleg.obtenirPerReferencia(referencia); // pot llancar
Empleat empleat = obtenirEmpleat(idEmpleat); // pot llancar
if (!material.estaDisponible()) {
throw new MaterialNoDisponibleException(referencia, material.getTitol(),
"desconegut", dia + Prestec.DIES_PRESTEC);
}
if (!empleat.potPrendrePrestat()) {
throw new LimitPrestecsExceditException(empleat.getIdentificador(),
empleat.getNom(), empleat.getPrestecsAcumulats(),
Empleat.MAX_PRESTECS_SIMULTANIS);
}
// --- Fase 2: ESCRIPTURA. Cada pas modifica estat i es marca ---
boolean materialMarcat = false;
boolean quotaConsumida = false;
boolean prestecAnotat = false;
String referenciaPrestec = null;
try {
material.prestar();
materialMarcat = true;
empleat.registrarPrestec();
quotaConsumida = true;
referenciaPrestec = seguentReferencia();
Prestec prestec = new Prestec(referenciaPrestec, material, empleat, dia);
prestecs.put(referenciaPrestec, prestec);
prestecAnotat = true;
// Ultim pas, tambe dins del try protegit
notificarCuaReserves(referencia);
return prestec;
} finally {
// Si NO s'ha completat l'ultim pas, desfem el que s'ha fet, en ordre invers.
// COMPTE: aqui NO hi ha return, ni throw, ni break. Nomes compensacio.
if (!prestecAnotat) {
System.out.println(" [compensacio] l'operacio ha fallat a mitges, desfent...");
if (referenciaPrestec != null) {
prestecs.remove(referenciaPrestec);
System.out.println(" - prestec " + referenciaPrestec + " retirat");
}
if (quotaConsumida) {
empleat.registrarDevolucio();
System.out.println(" - quota de " + idEmpleat + " restituida");
}
if (materialMarcat) {
material.retornar();
System.out.println(" - material " + referencia + " alliberat");
}
System.out.println(" [compensacio] estat restaurat");
}
}
}
/**
* Retorna un material deixant el sistema coherent.
* Mateix patro, en sentit invers.
*/
public void retornar(String referenciaPrestec, int dia) {
Objects.requireNonNull(referenciaPrestec, "La referencia no pot ser nulla");
Prestec prestec = prestecs.get(referenciaPrestec);
if (prestec == null) {
throw new MaterialNoTrobatException(referenciaPrestec, prestecs.size());
}
if (prestec.estaRetornat()) {
throw new PrestecJaRetornatException(referenciaPrestec, prestec.getDiaInici());
}
boolean devolucioRegistrada = false;
boolean quotaAlliberada = false;
boolean completada = false;
try {
prestec.registrarDevolucio(dia); // marca el prestec i allibera el material
devolucioRegistrada = true;
prestec.getTitular().registrarDevolucio();
quotaAlliberada = true;
notificarCuaReserves(prestec.getMaterial().getReferencia());
completada = true;
} finally {
if (!completada) {
System.out.println(" [compensacio] devolucio incompleta, desfent...");
if (quotaAlliberada) {
prestec.getTitular().registrarPrestec();
}
if (devolucioRegistrada) {
prestec.anullarDevolucio(); // metode de compensacio a Prestec
}
System.out.println(" [compensacio] estat restaurat");
}
}
}
private Empleat obtenirEmpleat(String idEmpleat) {
Empleat empleat = empleats.get(idEmpleat);
if (empleat == null) {
throw new MaterialNoTrobatException(idEmpleat, empleats.size());
}
return empleat;
}
/** Simula el pas que pot fallar (avisar el seguent de la cua de reserves). */
private void notificarCuaReserves(String referencia) {
if ("LIB-0003".equals(referencia)) {
throw new IllegalStateException(
"El servei d'avisos no respon per a " + referencia);
}
}
private String seguentReferencia() {
comptador++;
return String.format("PR-%04d", comptador);
}
public int prestecsRegistrats() { return prestecs.size(); }
}I la demostració que l'estat queda coherent:
package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.servei.GestorPrestecs;
public class DemoCoherencia {
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-0003", "Refactoritzacio", "Fowler", 1999, "978-0000000003"));
GestorPrestecs gestor = new GestorPrestecs(cataleg);
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
gestor.donarAlta(marta);
System.out.println("=== ESTAT INICIAL ===");
estat(cataleg, marta, gestor);
System.out.println("\n=== Prestec que FUNCIONA ===");
gestor.prestar("LIB-0001", "EMP-001", 10);
estat(cataleg, marta, gestor);
System.out.println("\n=== Prestec que FALLA a l'ultim pas ===");
try {
gestor.prestar("LIB-0003", "EMP-001", 10);
} catch (IllegalStateException e) {
System.out.println(" Excepcio rebuda: " + e.getMessage());
}
System.out.println("\n=== ESTAT DESPRES DE LA FALLADA ===");
estat(cataleg, marta, gestor);
System.out.println("""
COMPROVACIO:
- LIB-0003 torna a estar disponible (no ha quedat bloquejat).
- Marta conserva 1 prestec, no 2 (no ha perdut quota).
- No hi ha cap prestec fantasma al registre.
- I l'excepcio ha arribat a main INTACTA: el finally no l'ha amagat.""");
}
private static void estat(Cataleg cataleg, Empleat empleat, GestorPrestecs gestor) {
System.out.println(" LIB-0001 disponible: "
+ cataleg.obtenirPerReferencia("LIB-0001").estaDisponible());
System.out.println(" LIB-0003 disponible: "
+ cataleg.obtenirPerReferencia("LIB-0003").estaDisponible());
System.out.println(" " + empleat);
System.out.println(" Prestecs registrats: " + gestor.prestecsRegistrats());
}
}Sortida:
=== ESTAT INICIAL ===
LIB-0001 disponible: true
LIB-0003 disponible: true
EMP-001 (Marta Ruiz): 0/3 prestecs
Prestecs registrats: 0
=== Prestec que FUNCIONA ===
LIB-0001 disponible: false
LIB-0003 disponible: true
EMP-001 (Marta Ruiz): 1/3 prestecs
Prestecs registrats: 1
=== Prestec que FALLA a l'ultim pas ===
[compensacio] l'operacio ha fallat a mitges, desfent...
- prestec PR-0002 retirat
- quota de EMP-001 restituida
- material LIB-0003 alliberat
[compensacio] estat restaurat
Excepcio rebuda: El servei d'avisos no respon per a LIB-0003
=== ESTAT DESPRES DE LA FALLADA ===
LIB-0001 disponible: false
LIB-0003 disponible: true
EMP-001 (Marta Ruiz): 1/3 prestecs
Prestecs registrats: 1
COMPROVACIO:
- LIB-0003 torna a estar disponible (no ha quedat bloquejat).
- Marta conserva 1 prestec, no 2 (no ha perdut quota).
- No hi ha cap prestec fantasma al registre.
- I l'excepcio ha arribat a main INTACTA: el finally no l'ha amagat.Les quatre decisions de disseny que fan que aquest patró funcioni:
- Separar lectura d'escriptura. Tota la validació i la cerca passen abans de tocar res. Si alguna cosa falla a la fase 1, no hi ha res a desfer. És l'aplicació directa del fail-fast de 06-03: com més tard comencis a modificar, menys hauràs de compensar.
- Un marcador per cada pas que modifica estat. Sense ells, el
finallyno sabria quant va avançar l'operació i desfer de més seria tan greu com no desfer. - Desfer en ordre invers. Igual que es tanquen els recursos: l'últim pas completat és el primer que es reverteix.
- El
finallyno llança, no retorna i no captura. Només compensa. Per això l'excepció original arriba amainintacta —i per això, si la compensació mateixa pogués fallar, caldria embolcallar-la en el seu propitryintern per no perdre l'excepció principal, exactament el problema de l'apartat 8.
I una honestedat necessària sobre els límits d'aquesta tècnica: això és una compensació manual, no una transacció real. No és atòmica davant d'un altre fil que miri l'estat al mig de l'operació (mòdul 8), i no sobreviu a una caiguda de la JVM (apartat 9). Les transaccions de debò arriben al mòdul 11 amb Spring i Hibernate. Però per a una aplicació d'un sol fil com BiblioTech, resol exactament la fragilitat que teníem.
Errors Comuns i Consells
Posar un return al finally. Descarta el valor del try i —el que és greu— descarta qualsevol excepció pendent, convertint una fallada en un resultat normal. El mateix amb break, continue i throw. El finally neteja; no decideix.
Deixar que el finally llanci sense protegir-lo. Si el try ja havia llançat, l'excepció del finally la substitueix i l'original desapareix sense deixar rastre. Si el codi del finally pot fallar, embolcalla'l en el seu propi try intern... o fes servir try-with-resources (06-06), que és per al que existeix.
Declarar el recurs dins del try. El finally no el veu: no compila. Va fora, inicialitzat a null.
Oblidar la comprovació de null al finally. Si l'obertura del recurs ha fallat, la variable continua a null i el close() llança un NullPointerException que a més tapa la fallada original.
Tancar els recursos en l'ordre d'obertura. Cal fer-ho a l'inrevés. Tancar primer el FileReader i després el BufferedReader que l'embolcalla deixa el segon operant sobre un flux ja tancat.
Creure que finally s'executa sempre, sense excepcions. No ho fa amb System.exit() ni quan la JVM o el fil moren de manera anòmala. Per a la neteja global de l'aplicació hi ha els hooks d'aturada; per a la durabilitat, la persistència.
Fer servir finally per a lògica de negoci. Si el bloc fa alguna cosa més que netejar o compensar, és que està al lloc equivocat. finally és per desfer i alliberar, no per a "el pas final del procés".
Compensar sense marcadors. Desfer un pas que mai no va arribar a executar-se és tan destructiu com no desfer el que sí. Un marcador per pas modificador.
Consell: try-finally sense catch és una de les formes més elegants de Java. Significa exactament "no sé gestionar això, però netejo abans que se'n vagi", i deixa l'excepció intacta per a qui sí que en sàpiga.
Consell: valida tot abans de modificar res. Com més codi hi hagi entre la primera modificació i el final de l'operació, més superfície de compensació tens. El fail-fast de 06-03 redueix el problema en origen.
Consell: activa -Xlint:finally en compilar. Avisa dels finally que no poden acabar normalment, que és justament l'error de l'apartat 7.
Consell: en codi nou, no escriguis el patró manual de l'apartat 10. És aquí perquè entenguis què automatitza la lliçó següent, no perquè el facis servir.
Exercicis
Exercici 1: traça de l'ordre d'execució
Sense executar el codi, predigues la sortida exacta de cada mètode i el valor que retorna. Després executa'l i compara. Per a cada discrepància, explica la regla que et faltava.
public class Endevina {
static int metodeA() {
int x = 1;
try {
x = 2;
return x;
} finally {
x = 3;
System.out.println("A-finally, x=" + x);
}
}
static int metodeB() {
try {
return 1;
} finally {
return 2;
}
}
static String metodeC() {
StringBuilder sb = new StringBuilder("inici");
try {
return sb.toString();
} finally {
sb.append("-modificat");
}
}
static StringBuilder metodeD() {
StringBuilder sb = new StringBuilder("inici");
try {
return sb;
} finally {
sb.append("-modificat");
}
}
static int metodeE() {
try {
throw new IllegalStateException("fallada");
} finally {
System.out.println("E-finally");
}
}
static int metodeF() {
try {
throw new IllegalStateException("fallada");
} finally {
return -1;
}
}
static int metodeG() {
int total = 0;
for (int i = 1; i <= 3; i++) {
try {
if (i == 2) { continue; }
total += i;
} finally {
total += 10;
System.out.println("G-finally i=" + i + " total=" + total);
}
}
return total;
}
}Respon a més: per què metodeC i metodeD es comporten de manera diferent si el finally és idèntic?
Exercici 2: SessioCataleg amb bloqueig garantit
Escriu SessioCataleg a com.nexussoftware.bibliotech.servei, que simuli una sessió de treball sobre el catàleg amb bloqueig exclusiu.
Requisits:
- Camps:
bloquejat(boolean),operacionsRealitzades(int),sessionsObertes(comptador estàtic). void obrir(): si ja està bloquejat, llançaIllegalStateException; si no, bloqueja i incrementa el comptador de sessions.void tancar(): allibera el bloqueig. Ha de ser idempotent: cridar-lo dues vegades no ha de fallar.T executar(String nomOperacio, Supplier<T> operacio): mètode genèric usantjava.util.function.Supplier(04-06) que obre la sessió, executa l'operació i garanteix el tancament ambtry-finally, fins i tot si l'operació llança. Registra l'operació aoperacionsRealitzadesnomés si ha tingut èxit.void executarSenseResultat(String nom, Runnable operacio): variant per a operacions sense retorn.
Al main, demostra:
- Una operació correcta: la sessió es tanca i el comptador puja.
- Una operació que llança: l'excepció arriba a qui crida intacta i la sessió es tanca igualment.
- Que després de la fallada es pot obrir una sessió nova (el bloqueig no ha quedat encallat).
- Que
tancar()dues vegades seguides no falla. - Un intent d'obrir dues sessions alhora, que ha de fallar amb un missatge clar.
Exercici 3: transferència de préstec amb compensació
A Nexus Software és habitual que un empleat cedeixi un material a un altre sense retornar-lo a la biblioteca. Implementa TransferenciaPrestecs.transferir(String referenciaPrestec, String idDesti, int dia), que traspassi un préstec actiu d'un empleat a un altre.
L'operació té cinc passos que modifiquen estat:
- Alliberar la quota del titular actual.
- Consumir la quota de l'empleat destinatari.
- Canviar el titular del préstec.
- Anotar una
Incidenciade tipus transferència al préstec. - Notificar la cua d'avisos (aquest pas pot fallar).
Requisits:
- Valida tot abans de modificar res: el préstec existeix, no està retornat, el destinatari existeix, el destinatari no és el mateix titular actual i el destinatari té quota.
- Fes servir marcadors i un
finallyde compensació que desfaci en ordre invers els passos completats. - L'excepció original ha d'arribar a qui crida intacta.
- La compensació pot fallar al seu torn: protegeix-la per no perdre l'excepció principal, i si la compensació falla, afegeix-la com a suprimida a l'original amb
addSuppressed()(avança 06-06). - Escriu un
mainque demostri: una transferència correcta, una que falla al pas 5 amb l'estat restaurat, i una que falla a la validació (sense compensació, perquè no s'ha arribat a modificar res).
Verifica al final que la suma de préstecs acumulats de tots els empleats coincideix amb el nombre de préstecs actius, en tots els escenaris.
Solucions
Solució 1
Sortides i valors:
| Mètode | Sortida per consola | Retorna | Regla aplicada |
|---|---|---|---|
metodeA |
A-finally, x=3 |
2 | El valor del return es copia abans del finally. Modificar la variable després no canvia el que es retorna |
metodeB |
(res) | 2 | El return del finally guanya i descarta el del try |
metodeC |
(res) | "inici" |
toString() crea un String nou i immutable; modificar el StringBuilder després no l'afecta |
metodeD |
(res) | "inici-modificat" |
Es retorna la referència; el finally modifica l'objecte apuntat, i qui rep veu el canvi |
metodeE |
E-finally |
(llança IllegalStateException) |
El finally s'executa i l'excepció continua pujant |
metodeF |
(res) | -1 | El return del finally s'empassa l'excepció. La fallada desapareix |
metodeG |
tres línies (a sota) | 34 | El finally s'executa també amb continue |
Traça detallada de metodeG:
i=1: no hi ha continue, total += 1 -> 1; finally: total += 10 -> 11 G-finally i=1 total=11 i=2: continue, se salta total += 2; finally: total += 10 -> 21 G-finally i=2 total=21 i=3: total += 3 -> 24; finally: total += 10 -> 34 G-finally i=3 total=34 Retorna 34
Per què metodeC i metodeD difereixen amb un finally idèntic:
És la distinció entre valor i referència, i la clau està en què es copia en avaluar el return:
- A
metodeC,sb.toString()construeix un objecteStringnou amb el contingut actual ("inici"). AquestStringés immutable i no té cap connexió amb elStringBuilder. Elfinallymodifica elStringBuilder, que ja no importa a ningú. - A
metodeDes retorna la referència alStringBuilder. El que es copia és l'adreça, no el contingut. Elfinallymodifica l'objecte al qual apunta aquella adreça, i qui rep la referència veu el canvi.
És exactament el pas per valor de referències de 03-03: el valor de retorn es congela, però si aquell valor és una referència, l'objecte apuntat continua sent mutable.
I l'avís que donen metodeB i metodeF en compilar amb -Xlint:finally:
És el compilador dient-te literalment que aquell finally no deixarà sortir el que hi hagués en curs.
Solució 2
package com.nexussoftware.bibliotech.servei;
import java.util.function.Supplier;
/**
* Sessio de treball amb bloqueig exclusiu sobre el cataleg.
*
* El bloqueig s'allibera SEMPRE gracies a try-finally, fins i tot quan
* l'operacio llanca. L'excepcio, en canvi, arriba intacta a qui crida:
* aquesta classe neteja, no gestiona.
*/
public class SessioCataleg {
private static int sessionsObertes = 0;
private boolean bloquejat = false;
private int operacionsRealitzades = 0;
/**
* Obre la sessio adquirint el bloqueig.
*
* @throws IllegalStateException si ja hi ha una sessio oberta
*/
public void obrir() {
if (bloquejat) {
throw new IllegalStateException(
"Ja hi ha una sessio oberta sobre el cataleg. Tanqueu-la abans d'obrir-ne una altra.");
}
bloquejat = true;
sessionsObertes++;
System.out.println(" [sessio #" + sessionsObertes + " oberta]");
}
/**
* Tanca la sessio. Es IDEMPOTENT: cridar-lo sobre una sessio ja tancada
* no fa res i no falla. Es una bona practica que es recupera a 06-06
* en parlar del contracte de close().
*/
public void tancar() {
if (!bloquejat) {
return; // ja estava tancada: no es un error
}
bloquejat = false;
System.out.println(" [sessio tancada]");
}
/**
* Executa una operacio amb resultat dins d'una sessio.
*
* S'usa Supplier<T> de java.util.function (04-06). El <T> es un tipus
* generic ja existent; els generics propis son materia de 10-01.
*/
public <T> T executar(String nomOperacio, Supplier<T> operacio) {
System.out.println(" Executant '" + nomOperacio + "'");
obrir();
boolean completada = false;
try {
T resultat = operacio.get();
completada = true; // nomes arriba aqui si NO ha llancat
return resultat;
} finally {
// Neteja garantida. Sense return, sense throw: l'excepcio passa intacta.
if (completada) {
operacionsRealitzades++;
System.out.println(" [operacio '" + nomOperacio + "' comptabilitzada]");
} else {
System.out.println(" [operacio '" + nomOperacio
+ "' fallida: NO es comptabilitza]");
}
tancar();
}
}
/** Variant per a operacions sense resultat. */
public void executarSenseResultat(String nomOperacio, Runnable operacio) {
executar(nomOperacio, () -> {
operacio.run();
return null; // Supplier necessita retornar alguna cosa
});
}
public boolean estaBloquejat() { return bloquejat; }
public int getOperacionsRealitzades() { return operacionsRealitzades; }
public static int getSessionsObertes() { return sessionsObertes; }
// ------------------------------------------------------------------
public static void main(String[] args) {
SessioCataleg sessio = new SessioCataleg();
System.out.println("=== 1. Operacio correcta ===");
int total = sessio.executar("comptar materials", () -> {
System.out.println(" comptant...");
return 3;
});
System.out.println(" Resultat: " + total);
System.out.println(" Bloquejat: " + sessio.estaBloquejat()
+ " | Operacions: " + sessio.getOperacionsRealitzades());
System.out.println("\n=== 2. Operacio que llanca ===");
try {
sessio.executar("reindexar", () -> {
System.out.println(" reindexant...");
throw new IllegalStateException("Index corrupte a la posicio 7");
});
} catch (IllegalStateException e) {
System.out.println(" Excepcio rebuda INTACTA: " + e.getMessage());
}
System.out.println(" Bloquejat: " + sessio.estaBloquejat()
+ " | Operacions: " + sessio.getOperacionsRealitzades());
System.out.println("\n=== 3. Sessio nova despres de la fallada ===");
String titol = sessio.executar("llegir titol", () -> "Java Eficac");
System.out.println(" Resultat: " + titol);
System.out.println("\n=== 4. tancar() dues vegades ===");
sessio.tancar();
sessio.tancar();
System.out.println(" No ha fallat: tancar() es idempotent");
System.out.println("\n=== 5. Dues sessions alhora ===");
try {
sessio.executar("externa", () -> {
// Dins de l'operacio s'intenta obrir UNA ALTRA sessio
sessio.obrir();
return "impossible";
});
} catch (IllegalStateException e) {
System.out.println(" " + e.getMessage());
}
System.out.println(" Bloquejat despres de l'intent: " + sessio.estaBloquejat());
System.out.println("\n=== Resum ===");
System.out.println(" Sessions obertes en total : " + getSessionsObertes());
System.out.println(" Operacions completades : " + sessio.getOperacionsRealitzades());
}
}Sortida:
=== 1. Operacio correcta ===
Executant 'comptar materials'
[sessio #1 oberta]
comptant...
[operacio 'comptar materials' comptabilitzada]
[sessio tancada]
Resultat: 3
Bloquejat: false | Operacions: 1
=== 2. Operacio que llanca ===
Executant 'reindexar'
[sessio #2 oberta]
reindexant...
[operacio 'reindexar' fallida: NO es comptabilitza]
[sessio tancada]
Excepcio rebuda INTACTA: Index corrupte a la posicio 7
Bloquejat: false | Operacions: 1
=== 3. Sessio nova despres de la fallada ===
Executant 'llegir titol'
[sessio #3 oberta]
[operacio 'llegir titol' comptabilitzada]
[sessio tancada]
Resultat: Java Eficac
=== 4. tancar() dues vegades ===
No ha fallat: tancar() es idempotent
=== 5. Dues sessions alhora ===
Executant 'externa'
[sessio #4 oberta]
[operacio 'externa' fallida: NO es comptabilitza]
[sessio tancada]
Ja hi ha una sessio oberta sobre el cataleg. Tanqueu-la abans d'obrir-ne una altra.
Bloquejat despres de l'intent: false
=== Resum ===
Sessions obertes en total : 4
Operacions completades : 2El punt 2 és el que demostra el valor de try-finally sense catch: la sessió es va tancar, l'operació no es va comptabilitzar, i l'excepció va arribar a main amb el seu missatge original i la seva pila. La classe va netejar sense decidir.
I fixa't en el punt 5: encara que l'operació va intentar una cosa il·legal, el bloqueig va quedar alliberat. Sense el finally, el catàleg hauria quedat bloquejat per sempre i l'aplicació inservible.
Solució 3
package com.nexussoftware.bibliotech.servei;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import com.nexussoftware.bibliotech.domini.*;
/**
* Transferencia d'un prestec actiu entre empleats, amb compensacio
* garantida si l'operacio falla a mitges.
*
* Demostra:
* - Validar tot abans de modificar res.
* - Un marcador per pas modificador.
* - Compensacio en ordre invers dins del finally.
* - Proteccio de la propia compensacio amb addSuppressed (avanca 06-06).
*/
public class TransferenciaPrestecs {
private final Map<String, Prestec> prestecs;
private final Map<String, Empleat> empleats;
/** Referencia que fara fallar el pas 5, per a la demostracio. */
private String referenciaQueFallaEnNotificar = null;
public TransferenciaPrestecs(Map<String, Prestec> prestecs,
Map<String, Empleat> empleats) {
this.prestecs = Objects.requireNonNull(prestecs);
this.empleats = Objects.requireNonNull(empleats);
}
public void simularFalladaNotificacio(String referenciaPrestec) {
this.referenciaQueFallaEnNotificar = referenciaPrestec;
}
/**
* Transfereix un prestec actiu a un altre empleat.
*
* @throws MaterialNoTrobatException si el prestec o el desti no existeixen
* @throws PrestecJaRetornatException si el prestec ja s'ha retornat
* @throws IllegalArgumentException si el desti es el titular actual
* @throws LimitPrestecsExceditException si el desti no te quota
* @throws IllegalStateException si falla la notificacio (pas 5)
*/
public void transferir(String referenciaPrestec, String idDesti, int dia) {
// ================== FASE 1: VALIDACIO (no modifica res) ==================
Objects.requireNonNull(referenciaPrestec, "La referencia del prestec no pot ser nulla");
Objects.requireNonNull(idDesti, "L'identificador de desti no pot ser nul");
if (dia < 1) {
throw new IllegalArgumentException("El dia ha de ser 1 o posterior, i era: " + dia);
}
Prestec prestec = prestecs.get(referenciaPrestec);
if (prestec == null) {
throw new MaterialNoTrobatException(referenciaPrestec, prestecs.size());
}
if (prestec.estaRetornat()) {
throw new PrestecJaRetornatException(referenciaPrestec, prestec.getDiaInici());
}
Empleat desti = empleats.get(idDesti);
if (desti == null) {
throw new MaterialNoTrobatException(idDesti, empleats.size());
}
Empleat origen = prestec.getTitular();
if (origen.getIdentificador().equals(idDesti)) {
throw new IllegalArgumentException(
"El prestec " + referenciaPrestec + " ja pertany a " + idDesti);
}
if (!desti.potPrendrePrestat()) {
throw new LimitPrestecsExceditException(desti.getIdentificador(),
desti.getNom(), desti.getPrestecsAcumulats(),
Empleat.MAX_PRESTECS_SIMULTANIS);
}
// ================== FASE 2: MODIFICACIO (amb marcadors) ==================
boolean quotaOrigenAlliberada = false;
boolean quotaDestiConsumida = false;
boolean titularCanviat = false;
boolean incidenciaAnotada = false;
boolean completada = false;
try {
// Pas 1
origen.registrarDevolucio();
quotaOrigenAlliberada = true;
// Pas 2
desti.registrarPrestec();
quotaDestiConsumida = true;
// Pas 3
prestec.canviarTitular(desti);
titularCanviat = true;
// Pas 4
prestec.anotarIncidencia(new Prestec.Incidencia(dia,
"Transferit de " + origen.getIdentificador() + " a " + idDesti,
Gravetat.SENSE_RETARD));
incidenciaAnotada = true;
// Pas 5: el que pot fallar
notificarCuaAvisos(referenciaPrestec, idDesti);
completada = true;
System.out.println(" Transferencia " + referenciaPrestec + ": "
+ origen.getIdentificador() + " -> " + idDesti + " OK");
} finally {
if (!completada) {
compensar(prestec, origen, desti,
quotaOrigenAlliberada, quotaDestiConsumida,
titularCanviat, incidenciaAnotada);
}
}
}
/**
* Desfa els passos completats, en ORDRE INVERS.
*
* Si la compensacio falla, NO es rellanca: aixo substituiria l'excepcio
* original (apartat 8). Es registra com a SUPRIMIDA a l'excepcio en curs.
*/
private void compensar(Prestec prestec, Empleat origen, Empleat desti,
boolean quotaOrigenAlliberada, boolean quotaDestiConsumida,
boolean titularCanviat, boolean incidenciaAnotada) {
System.out.println(" [compensacio] transferencia incompleta, desfent...");
RuntimeException falladaCompensacio = null;
// Ordre invers: 4 -> 3 -> 2 -> 1
if (incidenciaAnotada) {
try {
prestec.retirarUltimaIncidencia();
System.out.println(" - incidencia retirada");
} catch (RuntimeException e) {
falladaCompensacio = acumular(falladaCompensacio, e);
}
}
if (titularCanviat) {
try {
prestec.canviarTitular(origen);
System.out.println(" - titular restituit a " + origen.getIdentificador());
} catch (RuntimeException e) {
falladaCompensacio = acumular(falladaCompensacio, e);
}
}
if (quotaDestiConsumida) {
try {
desti.registrarDevolucio();
System.out.println(" - quota de " + desti.getIdentificador() + " restituida");
} catch (RuntimeException e) {
falladaCompensacio = acumular(falladaCompensacio, e);
}
}
if (quotaOrigenAlliberada) {
try {
origen.registrarPrestec();
System.out.println(" - quota de " + origen.getIdentificador() + " restituida");
} catch (RuntimeException e) {
falladaCompensacio = acumular(falladaCompensacio, e);
}
}
if (falladaCompensacio != null) {
// No podem llancar: perdriem l'excepcio original.
// Ho deixem registrat. A 06-07 aixo sera un logger.log(SEVERE, ...).
System.err.println(" [compensacio] AVIS: la compensacio ha fallat parcialment: "
+ falladaCompensacio.getMessage());
} else {
System.out.println(" [compensacio] estat restaurat completament");
}
}
private RuntimeException acumular(RuntimeException acumulada, RuntimeException nova) {
if (acumulada == null) {
return nova;
}
acumulada.addSuppressed(nova); // es veuran amb getSuppressed() (06-06)
return acumulada;
}
private void notificarCuaAvisos(String referenciaPrestec, String idDesti) {
if (referenciaPrestec.equals(referenciaQueFallaEnNotificar)) {
throw new IllegalStateException(
"El servei d'avisos no respon en notificar " + referenciaPrestec
+ " a " + idDesti);
}
System.out.println(" (avis enviat a " + idDesti + ")");
}
// ------------------------------------------------------------------
public static void main(String[] args) {
Cataleg cataleg = new Cataleg();
Llibre l1 = new Llibre("LIB-0001", "Java Eficac", "Bloch", 2018, "978-0000000001");
Llibre l2 = new Llibre("LIB-0002", "Patrons de Disseny", "GoF", 1994, "978-0000000002");
cataleg.registrar(l1);
cataleg.registrar(l2);
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
Empleat diego = new Empleat("Diego Alonso", "EMP-002");
Empleat nuria = new Empleat("Nuria Vidal", "EMP-003");
Map<String, Empleat> empleats = new HashMap<>();
empleats.put("EMP-001", marta);
empleats.put("EMP-002", diego);
empleats.put("EMP-003", nuria);
// El constructor de Prestec ja crida material.prestar() i
// titular.registrarPrestec(): no cal fer-ho a ma (06-03).
Map<String, Prestec> prestecs = new HashMap<>();
prestecs.put("PR-0001", new Prestec("PR-0001", l1, marta, 10));
prestecs.put("PR-0002", new Prestec("PR-0002", l2, marta, 10));
TransferenciaPrestecs servei = new TransferenciaPrestecs(prestecs, empleats);
System.out.println("=== ESTAT INICIAL ===");
estat(empleats, prestecs);
System.out.println("\n=== 1. Transferencia correcta ===");
servei.transferir("PR-0001", "EMP-002", 12);
estat(empleats, prestecs);
System.out.println("\n=== 2. Transferencia que falla al pas 5 ===");
servei.simularFalladaNotificacio("PR-0002");
try {
servei.transferir("PR-0002", "EMP-003", 13);
} catch (IllegalStateException e) {
System.out.println(" Excepcio rebuda INTACTA: " + e.getMessage());
}
estat(empleats, prestecs);
System.out.println("\n=== 3. Fallada a la validacio (sense compensacio) ===");
try {
servei.transferir("PR-9999", "EMP-003", 14);
} catch (MaterialNoTrobatException e) {
System.out.println(" " + e.getMessage());
System.out.println(" (no hi ha hagut compensacio: no s'ha modificat res)");
}
estat(empleats, prestecs);
}
/** Comprova l'invariant global del sistema. */
private static void estat(Map<String, Empleat> empleats, Map<String, Prestec> prestecs) {
int sumaQuotes = 0;
for (Empleat e : empleats.values()) {
System.out.println(" " + e);
sumaQuotes += e.getPrestecsAcumulats();
}
int actius = 0;
for (Prestec p : prestecs.values()) {
if (!p.estaRetornat()) { actius++; }
System.out.println(" " + p.getReferencia() + " -> "
+ p.getTitular().getIdentificador());
}
System.out.println(" INVARIANT: suma de quotes (" + sumaQuotes
+ ") == prestecs actius (" + actius + ") -> "
+ (sumaQuotes == actius ? "OK" : "TRENCAT"));
}
}Sortida:
=== ESTAT INICIAL ===
EMP-001 (Marta Ruiz): 2/3 prestecs
EMP-002 (Diego Alonso): 0/3 prestecs
EMP-003 (Nuria Vidal): 0/3 prestecs
PR-0001 -> EMP-001
PR-0002 -> EMP-001
INVARIANT: suma de quotes (2) == prestecs actius (2) -> OK
=== 1. Transferencia correcta ===
(avis enviat a EMP-002)
Transferencia PR-0001: EMP-001 -> EMP-002 OK
EMP-001 (Marta Ruiz): 1/3 prestecs
EMP-002 (Diego Alonso): 1/3 prestecs
EMP-003 (Nuria Vidal): 0/3 prestecs
PR-0001 -> EMP-002
PR-0002 -> EMP-001
INVARIANT: suma de quotes (2) == prestecs actius (2) -> OK
=== 2. Transferencia que falla al pas 5 ===
[compensacio] transferencia incompleta, desfent...
- incidencia retirada
- titular restituit a EMP-001
- quota de EMP-003 restituida
- quota de EMP-001 restituida
[compensacio] estat restaurat completament
Excepcio rebuda INTACTA: El servei d'avisos no respon en notificar PR-0002 a EMP-003
EMP-001 (Marta Ruiz): 1/3 prestecs
EMP-002 (Diego Alonso): 1/3 prestecs
EMP-003 (Nuria Vidal): 0/3 prestecs
PR-0001 -> EMP-002
PR-0002 -> EMP-001
INVARIANT: suma de quotes (2) == prestecs actius (2) -> OK
=== 3. Fallada a la validacio (sense compensacio) ===
No existeix cap material amb la referencia 'PR-9999' (el cataleg te 2 materials)
(no hi ha hagut compensacio: no s'ha modificat res)
INVARIANT: suma de quotes (2) == prestecs actius (2) -> OKLes tres coses que demostra l'exercici:
- L'invariant global es manté en els tres escenaris. La suma de quotes consumides coincideix sempre amb el nombre de préstecs actius, fins i tot després d'una fallada a mitges d'una operació de cinc passos.
- L'excepció arriba intacta. El
finallyva compensar i no va amagar res:mainva rebre l'IllegalStateExceptionamb el seu missatge original. - La compensació està protegida. Cada pas de desfer va al seu propi
try, i les fallades de compensació s'acumulen ambaddSuppressed()en comptes de rellançar-se. Si es rellancessin, substituirien l'excepció original —l'error de l'apartat 8— i perdríem el motiu real de la fallada.
Aquell ús d'addSuppressed() no és casual: és exactament el mecanisme que try-with-resources aplica automàticament, i que veuràs en detall a la lliçó següent.
Conclusió
Ja coneixes la garantia de finally i els seus límits exactes. Saps que s'executa sempre que s'hagi entrat al try: sense excepció, amb excepció capturada, amb excepció no capturada —just abans que continuï pujant— i també quan el try o el catch fan return, break o continue. I saps que amb try imbricats els finally es disparen de dins cap a fora, al mateix ritme al qual es desapilen els marcs, de manera que cada nivell neteja el que és seu sense saber res dels altres.
Entens la mecànica precisa del return: el valor es calcula i es guarda abans d'executar el finally, així que modificar la variable després no canvia el que es retorna —tret que el que es retorni sigui una referència a un objecte mutable, cas en què l'objecte sí que pot canviar, com van demostrar metodeC i metodeD.
Coneixes les combinacions vàlides —try-catch, try-catch-finally i try-finally, sempre amb el finally l'últim— i el valor específic de try-finally sense catch: "no sé gestionar això, però netejo abans que se'n vagi", que separa netejar de gestionar i deixa l'excepció intacta per a qui sí que sàpiga decidir.
Tens els dos paranys que cal evitar sense excepcions. El primer: un return —o break, continue, throw— dins del finally descarta el valor del try i, molt pitjor, s'empassa qualsevol excepció pendent, convertint un catàleg corrupte en un tranquil -1. El segon: un finally que llança la seva pròpia excepció substitueix l'original, que desapareix sense causa ni suprimides —el cas realíssim del close() que falla i esborra la fallada veritable. Has vist el pedaç manual de tretze línies que ho evita, i saps que existeix una construcció que fa tot això per tu.
Saps que la garantia no és absoluta: finally no s'executa amb System.exit() ni quan la JVM o el fil moren de manera anòmala, i que per a la neteja global de l'aplicació hi ha els hooks d'aturada —que sí que corren amb System.exit, però tampoc amb un halt() o un SIGKILL. D'aquí la conclusió que convé retenir: finally garanteix coherència dins del procés, no durabilitat fora d'ell.
I has vist el patró manual de tancament anterior a Java 7 en tota la seva extensió: declarar fora, inicialitzar a null, comprovar != null, embolcallar el close() al seu propi try, no rellançar des d'allà, tancar en ordre invers i imbricar un try/finally per cada recurs addicional. Set requisits que gairebé ningú no complia, i vint línies de comptabilitat per a quatre de feina real.
BiblioTech ha resolt la quarta fragilitat del mòdul 5. GestorPrestecs.prestar i retornar separen la fase de lectura de la d'escriptura, marquen amb un marcador cada pas que modifica estat i compensen en ordre invers dins d'un finally que no llança, no retorna i no captura. El resultat, demostrat amb l'invariant "suma de quotes consumides == préstecs actius": quan la notificació a la cua d'avisos falla a l'últim pas, el material torna a estar disponible, l'empleat recupera la seva quota, no queda cap préstec fantasma i l'excepció arriba a main intacta. Amb l'honestedat de reconèixer que això és compensació manual, no una transacció real: no és atòmica davant d'un altre fil (mòdul 8) ni sobreviu a una caiguda de la JVM, i les transaccions de debò arriben amb Spring i Hibernate al mòdul 11.
De la llista de fragilitats del mòdul 5 només queda l'última, la de la persistència, que és el mòdul 7. Però abans cal tancar el deute que aquesta lliçó ha deixat obert dues vegades: el finally que perd l'excepció original en tancar, i les vint línies del patró manual.
Això és la lliçó següent, Try-with-resources i AutoCloseable: la sintaxi de gestió automàtica de recursos de Java 7 i la seva equivalència exacta amb el try-finally manual, mostrades en paral·lel; les interfícies AutoCloseable i Closeable i la seva diferència; diversos recursos a la mateixa declaració amb ordre de tancament invers al d'obertura, demostrat amb traces; la variable implícitament final i la forma de Java 9 que admet una variable ja existent; les excepcions suprimides, que resolen exactament el problema de l'apartat 8 conservant l'original i deixant la del tancament accessible amb getSuppressed(); com combinar try-with-resources amb catch i finally; quins recursos no s'han de tancar així —començant per System.in i el Scanner del mòdul 1—; i com fer que les teves pròpies classes siguin tancables, amb una SessioBiblioteca que obri un torn de treball i en tancar-se consolidi les estadístiques i alliberi els bloquejos.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
