La lección anterior dejó dos deudas explícitas. La primera: el patrón manual de cierre de recursos, con sus siete requisitos y sus veinte líneas de contabilidad para cuatro de trabajo real. La segunda, más grave: el finally que lanza su propia excepción al cerrar y hace desaparecer el fallo original, dejándote con un mensaje sobre el cierre y ni una pista del problema verdadero.
Java 7 resolvió las dos de golpe con una sola construcción: el try-with-resources. Declaras los recursos entre paréntesis, y el lenguaje se encarga de cerrarlos siempre, en el orden correcto, y de conservar la excepción original registrando la del cierre como suprimida en lugar de dejar que la sustituya.
Pero esta lección no va solo de usar la construcción. Va de entender qué hace exactamente por debajo —porque su equivalencia con el try-finally manual es exacta y se puede escribir— y de aprender a hacer que tus propias clases sean cerrables. Al terminar, BiblioTech tendrá una SesionBiblioteca que abre un turno de trabajo y, al cerrarse, consolida las estadísticas y libera los bloqueos, se salga del bloque como se salga.
Aviso de alcance: varios ejemplos usan
FileReader,BufferedReaderyScannersobre fichero. La entrada/salida es el módulo 7, y allí se explica esa API a fondo. Aquí se usa lo mínimo imprescindible, y el foco está siempre en el cierre, no en la lectura.
Contenido
- La sintaxis y su equivalencia exacta
AutoCloseableyCloseable- Varios recursos: orden de cierre inverso
- La variable del recurso es implícitamente
final - La forma de Java 9: variables ya existentes
- Excepciones suprimidas: el problema resuelto
getSuppressed()y cómo aparecen en el stack trace- Combinar con
catchyfinally - Recursos que NO deben cerrarse así
- Implementar
AutoCloseableen tus clases - Buenas prácticas en
close() - BiblioTech:
SesionBibliotecayExportadorCatalogo - Tabla resumen: gestión manual frente a automática
- Errores Comunes y Consejos
- Ejercicios
- La sintaxis y su equivalencia exacta
La forma es un try con una lista de declaraciones entre paréntesis:
Y ahora, los dos códigos en paralelo. Este es el patrón manual correcto de 06-05:
// ANTES DE JAVA 7: gestion manual
public int contarLineas(String ruta) throws IOException {
BufferedReader lector = null; // 1. declarar fuera
try {
lector = new BufferedReader(new FileReader(ruta));
int lineas = 0;
while (lector.readLine() != null) {
lineas++;
}
return lineas;
} finally {
if (lector != null) { // 2. comprobar null
try {
lector.close(); // 3. close() lanza IOException
} catch (IOException eCierre) {
// 4. no relanzar: perderiamos la excepcion original
System.err.println("Aviso: " + eCierre.getMessage());
}
}
}
}Y este es el equivalente con try-with-resources:
// DESDE JAVA 7: gestion automatica
public int contarLineas(String ruta) throws IOException {
try (BufferedReader lector = new BufferedReader(new FileReader(ruta))) {
int lineas = 0;
while (lector.readLine() != null) {
lineas++;
}
return lineas;
}
}Nueve líneas de contabilidad convertidas en cero, y con mejor comportamiento que la versión manual, porque la excepción del cierre no se pierde: queda registrada como suprimida.
La equivalencia exacta que genera el compilador, para un solo recurso, es aproximadamente esta:
// Lo que el compilador genera (esquema simplificado)
BufferedReader lector = new BufferedReader(new FileReader(ruta));
Throwable excepcionPrincipal = null;
try {
// ... cuerpo del try ...
} catch (Throwable t) {
excepcionPrincipal = t;
throw t;
} finally {
if (lector != null) {
if (excepcionPrincipal != null) {
try {
lector.close();
} catch (Throwable tCierre) {
excepcionPrincipal.addSuppressed(tCierre); // <-- la clave
}
} else {
lector.close(); // sin excepcion previa: si esta lanza, se propaga
}
}
}Fíjate en la línea marcada: es exactamente el addSuppressed() que tuviste que escribir a mano en el ejercicio 3 de 06-05. Aquí lo pone el compilador por ti, en todos los casos y sin posibilidad de olvidarlo.
Un detalle importante de esa expansión: si no hubo excepción en el cuerpo, el close() se llama sin protección, así que si el cierre falla, esa excepción sí se propaga. Es lo correcto: si nada más falló, el fallo del cierre es la única noticia y debe llegar arriba.
AutoCloseable y Closeable
AutoCloseable y CloseablePara que una clase pueda usarse en un try-with-resources, debe implementar una de estas dos interfaces:
// Java 7, java.lang
public interface AutoCloseable {
void close() throws Exception;
}
// Java 5, java.io
public interface Closeable extends AutoCloseable {
void close() throws IOException;
}Sus diferencias:
AutoCloseable |
Closeable |
|
|---|---|---|
| Paquete | java.lang |
java.io |
| Desde | Java 7 | Java 5 |
close() declara |
throws Exception |
throws IOException |
| Idempotencia | No exigida (pero recomendada) | Exigida por el contrato |
| Relación | Es la superinterfaz | Extiende AutoCloseable |
| Cuándo elegirla | Recursos generales: sesiones, bloqueos, transacciones | Recursos de E/S |
La diferencia práctica más importante es la excepción declarada. Si tu clase implementa AutoCloseable sin restringir el throws, el compilador obligará a quien la use a manejar una Exception genérica:
// close() declara Exception: el usuario tiene que capturar Exception, demasiado ancho
class SesionAmplia implements AutoCloseable {
@Override
public void close() throws Exception { }
}
try (SesionAmplia s = new SesionAmplia()) {
// ...
} catch (Exception e) { // obligado a capturar Exception: horrible
// ...
}La solución, y es la práctica correcta: restringe el throws en tu implementación. Recuerda de 06-03 que una sobrescritura puede declarar menos excepciones que el método original, nunca más:
// BIEN: close() sin throws. Quien lo use no tendra que capturar nada.
class SesionLimpia implements AutoCloseable {
@Override
public void close() { // sin throws: mas restrictivo, perfectamente legal
// liberar recursos sin lanzar
}
}
try (SesionLimpia s = new SesionLimpia()) {
// ...
}
// Sin catch obligatorio. Mucho mejor.Regla práctica: si tu
close()no necesita lanzar una excepción comprobada, no la declares. Cadathrowsque pongas ahí se lo estás imponiendo a todos tus usuarios, en cadatry-with-resources.
Estas son las clases de la biblioteca estándar que ya son cerrables y que te encontrarás:
| Clase | Interfaz | Módulo del curso |
|---|---|---|
BufferedReader, FileReader, FileWriter |
Closeable |
7 |
Scanner |
Closeable |
1 y 7 |
InputStream, OutputStream y sus subclases |
Closeable |
7 |
Files.newBufferedReader(...) |
Closeable |
7 |
Connection, Statement, ResultSet (JDBC) |
AutoCloseable |
11 |
Socket, ServerSocket |
Closeable |
9 |
ExecutorService (desde Java 19) |
AutoCloseable |
8 |
Stream |
AutoCloseable |
10 |
- Varios recursos: orden de cierre inverso
Puedes declarar varios recursos separados por punto y coma. Se cierran en orden inverso al de apertura, que es lo que necesitas cuando un recurso envuelve a otro.
package com.nexussoftware.bibliotech.presentacion;
/**
* Demuestra el orden de apertura y de cierre con tres recursos.
* Los recursos son simulados: no hay E/S real, para que el foco este
* exclusivamente en el ORDEN.
*/
public class OrdenDeCierre {
/** Recurso de juguete que anuncia su apertura y su cierre. */
static class RecursoTrazado implements AutoCloseable {
private final String nombre;
RecursoTrazado(String nombre) {
this.nombre = nombre;
System.out.println(" ABRIR " + nombre);
}
void usar() {
System.out.println(" USAR " + nombre);
}
@Override
public void close() { // sin throws: mas comodo para quien lo use
System.out.println(" CERRAR " + nombre);
}
}
public static void main(String[] args) {
System.out.println("=== Sin excepcion ===");
try (RecursoTrazado a = new RecursoTrazado("A-fichero");
RecursoTrazado b = new RecursoTrazado("B-buffer");
RecursoTrazado c = new RecursoTrazado("C-parser")) {
a.usar();
b.usar();
c.usar();
}
System.out.println("\n=== Con excepcion en el cuerpo ===");
try (RecursoTrazado a = new RecursoTrazado("A-fichero");
RecursoTrazado b = new RecursoTrazado("B-buffer")) {
a.usar();
throw new IllegalStateException("fallo en mitad del proceso");
} catch (IllegalStateException e) {
System.out.println(" CATCH " + e.getMessage());
}
System.out.println("\n=== Con excepcion al ABRIR el segundo recurso ===");
try (RecursoTrazado a = new RecursoTrazado("A-fichero");
RecursoTrazado b = crearQueFalla("B-buffer")) {
a.usar();
} catch (IllegalStateException e) {
System.out.println(" CATCH " + e.getMessage());
}
}
static RecursoTrazado crearQueFalla(String nombre) {
throw new IllegalStateException("no se pudo abrir " + nombre);
}
}Salida:
=== Sin excepcion === ABRIR A-fichero ABRIR B-buffer ABRIR C-parser USAR A-fichero USAR B-buffer USAR C-parser CERRAR C-parser CERRAR B-buffer CERRAR A-fichero === Con excepcion en el cuerpo === ABRIR A-fichero ABRIR B-buffer USAR A-fichero CERRAR B-buffer CERRAR A-fichero CATCH fallo en mitad del proceso === Con excepcion al ABRIR el segundo recurso === ABRIR A-fichero CERRAR A-fichero CATCH no se pudo abrir B-buffer
Tres observaciones clave:
El cierre es inverso a la apertura. C, B, A. Es imprescindible cuando los recursos se envuelven: un BufferedReader construido sobre un FileReader debe cerrarse antes que el FileReader, porque su close() vacía el búfer escribiendo en el flujo subyacente. Si lo cerraras al revés, escribirías sobre un flujo ya cerrado.
Los recursos se cierran antes que el catch. En el segundo caso, CERRAR B y CERRAR A salen antes de CATCH. Esto es lo que quieres: cuando el manejador se ejecuta, los recursos ya están liberados y puede usarlos para otra cosa, o reintentar.
Si falla la apertura del segundo recurso, el primero se cierra igual. Este caso es el que el patrón manual hacía mal casi siempre: con el try-finally de 06-05 había que anidar un bloque por recurso para conseguirlo, y bastaba un descuido para dejar el primero abierto.
- La variable del recurso es implícitamente
final
finalDentro del bloque, el recurso no se puede reasignar:
try (BufferedReader lector = new BufferedReader(new FileReader("catalogo.txt"))) {
lector = new BufferedReader(new FileReader("otro.txt")); // NO COMPILA
// error: auto-closeable resource lector may not be assigned
}La razón es evidente si piensas en la expansión del apartado 1: el finally generado cierra la variable. Si pudieras reasignarla, cerrarías el segundo recurso y dejarías el primero abierto para siempre. La restricción impide una fuga garantizada.
Puedes escribir final explícitamente si te gusta la claridad, aunque es redundante:
Y el alcance del recurso es el bloque try. Fuera de él no existe, ni siquiera en los catch o el finally del mismo try:
try (RecursoTrazado r = new RecursoTrazado("A")) {
r.usar();
} catch (Exception e) {
r.usar(); // NO COMPILA: cannot find symbol
} finally {
r.usar(); // NO COMPILA: cannot find symbol
}Tiene sentido: cuando se ejecutan el catch y el finally, el recurso ya está cerrado. Que no sea accesible te impide usarlo por error en ese estado.
- La forma de Java 9: variables ya existentes
En Java 7 y 8, el recurso tenía que declararse dentro de los paréntesis. Esto obligaba a un rodeo incómodo cuando el recurso venía de fuera:
// Java 7/8: hay que crear una variable nueva solo para poder usarla
public void procesar(BufferedReader lectorRecibido) throws IOException {
try (BufferedReader lector = lectorRecibido) { // variable redundante
System.out.println(lector.readLine());
}
}Desde Java 9 puedes usar directamente una variable final o efectivamente final ya existente:
// Java 9+: se usa la variable tal cual
public void procesar(BufferedReader lectorRecibido) throws IOException {
try (lectorRecibido) { // sin declaracion nueva
System.out.println(lectorRecibido.readLine());
}
}Efectivamente final significa lo mismo que en las lambdas de 04-05: una variable que no se reasigna después de su inicialización, aunque no lleve la palabra final.
public void demo() throws Exception {
RecursoTrazado a = new RecursoTrazado("A"); // efectivamente final: nunca se reasigna
RecursoTrazado b = new RecursoTrazado("B");
try (a; b) { // varios, separados por ;
a.usar();
b.usar();
}
// Se cierran B y luego A
}Y si la variable sí se reasigna, no compila:
RecursoTrazado r = new RecursoTrazado("A");
r = new RecursoTrazado("B"); // reasignada: ya no es efectivamente final
try (r) { // NO COMPILA
// error: local variables referenced from a resource specification
// must be final or effectively final
}Un aviso sobre cuándo usar esta forma. Es cómoda, pero conlleva una decisión de diseño: ¿quién es el dueño del recurso? Si un método recibe un BufferedReader como parámetro y lo cierra, está tomando una decisión que probablemente le corresponde a quien se lo pasó:
// SOSPECHOSO: cierro algo que no he abierto yo
public void procesar(BufferedReader lector) throws IOException {
try (lector) { // ¿tengo derecho a cerrarlo?
// ...
}
}
// Quien llame no podra volver a usar su lector, y quiza no lo esperaba.La regla general: cierra lo que abres. Si recibes un recurso ya abierto, lo normal es usarlo y devolverlo intacto, dejando el cierre a quien lo creó. La forma de Java 9 es más útil cuando el recurso se ha creado en el mismo método pero en varios pasos, o cuando el método documenta explícitamente que toma posesión del recurso.
- Excepciones suprimidas: el problema resuelto
Aquí es donde try-with-resources demuestra que es mucho más que azúcar sintáctico. Recupera el problema exacto de 06-05: el try lanza el fallo real y el close() lanza otro, y con finally manual la segunda borraba la primera.
package com.nexussoftware.bibliotech.presentacion;
/**
* Compara que ocurre cuando fallan el cuerpo Y el cierre:
* - con try-finally manual: la excepcion del cierre SUSTITUYE a la original.
* - con try-with-resources: la original se conserva y la del cierre queda SUPRIMIDA.
*/
public class ExcepcionesSuprimidas {
/** Recurso cuyo cierre tambien falla. */
static class RecursoFragil implements AutoCloseable {
private final String nombre;
RecursoFragil(String nombre) {
this.nombre = nombre;
}
void leer() {
throw new IllegalStateException("FALLO REAL: " + nombre + " esta corrupto");
}
@Override
public void close() {
throw new IllegalStateException("FALLO AL CERRAR: no se libero " + nombre);
}
}
public static void main(String[] args) {
System.out.println("=== A) try-finally MANUAL ===");
try {
conFinallyManual();
} catch (IllegalStateException e) {
System.out.println(" Recibida : " + e.getMessage());
System.out.println(" Causa : " + e.getCause());
System.out.println(" Suprimidas: " + e.getSuppressed().length);
System.out.println(" >> El FALLO REAL ha desaparecido");
}
System.out.println("\n=== B) try-with-resources ===");
try {
conTryWithResources();
} catch (IllegalStateException e) {
System.out.println(" Recibida : " + e.getMessage());
System.out.println(" Causa : " + e.getCause());
System.out.println(" Suprimidas: " + e.getSuppressed().length);
for (Throwable suprimida : e.getSuppressed()) {
System.out.println(" - " + suprimida.getMessage());
}
System.out.println(" >> Se conservan LAS DOS");
}
System.out.println("\n=== C) Solo falla el cierre ===");
try {
soloFallaElCierre();
} catch (IllegalStateException e) {
System.out.println(" Recibida : " + e.getMessage());
System.out.println(" Suprimidas: " + e.getSuppressed().length);
System.out.println(" >> Sin excepcion previa, la del cierre se propaga normal");
}
}
static void conFinallyManual() {
RecursoFragil recurso = new RecursoFragil("catalogo.txt");
try {
recurso.leer();
} finally {
recurso.close(); // esta excepcion SUSTITUYE a la del try
}
}
static void conTryWithResources() {
try (RecursoFragil recurso = new RecursoFragil("catalogo.txt")) {
recurso.leer();
}
}
static void soloFallaElCierre() {
try (RecursoFragil recurso = new RecursoFragil("prestamos.txt")) {
System.out.println(" (el cuerpo termina bien)");
}
}
}Salida:
=== A) try-finally MANUAL ===
Recibida : FALLO AL CERRAR: no se libero catalogo.txt
Causa : null
Suprimidas: 0
>> El FALLO REAL ha desaparecido
=== B) try-with-resources ===
Recibida : FALLO REAL: catalogo.txt esta corrupto
Causa : null
Suprimidas: 1
- FALLO AL CERRAR: no se libero catalogo.txt
>> Se conservan LAS DOS
=== C) Solo falla el cierre ===
(el cuerpo termina bien)
Recibida : FALLO AL CERRAR: no se libero prestamos.txt
Suprimidas: 0
>> Sin excepcion previa, la del cierre se propaga normalLas reglas del mecanismo de supresión, completas:
| Situación | Qué se propaga | Qué queda suprimido |
|---|---|---|
| Falla solo el cuerpo | La del cuerpo | Nada |
| Falla solo el cierre | La del cierre | Nada |
| Fallan cuerpo y cierre | La del cuerpo | La del cierre |
| Fallan cuerpo y varios cierres | La del cuerpo | Todas las de cierre |
La prioridad es deliberada y correcta: el fallo del cuerpo es el que explica qué salió mal; el del cierre suele ser una consecuencia. Pero ninguna de las dos se pierde.
Y un detalle sobre la diferencia entre causa y suprimida, que se confunden:
Causa (getCause) |
Suprimida (getSuppressed) |
|
|---|---|---|
| Relación | A provocó B | A y B ocurrieron independientemente |
| Cardinalidad | Una sola, encadenada | Varias, en un array |
| Se establece con | El constructor con causa, o initCause |
addSuppressed(), casi siempre automático |
| Ejemplo | NumberFormatException causó IllegalStateException |
El fichero estaba corrupto y además no se pudo cerrar |
getSuppressed() y cómo aparecen en el stack trace
getSuppressed() y cómo aparecen en el stack traceLas excepciones suprimidas aparecen en el volcado con la etiqueta Suppressed:, indentadas bajo la principal:
Exception in thread "main" java.lang.IllegalStateException: FALLO REAL: catalogo.txt esta corrupto at com.nexussoftware.bibliotech.presentacion.ExcepcionesSuprimidas$RecursoFragil.leer(ExcepcionesSuprimidas.java:22) at com.nexussoftware.bibliotech.presentacion.ExcepcionesSuprimidas.conTryWithResources(ExcepcionesSuprimidas.java:70) at com.nexussoftware.bibliotech.presentacion.ExcepcionesSuprimidas.main(ExcepcionesSuprimidas.java:38) Suppressed: java.lang.IllegalStateException: FALLO AL CERRAR: no se libero catalogo.txt at com.nexussoftware.bibliotech.presentacion.ExcepcionesSuprimidas$RecursoFragil.close(ExcepcionesSuprimidas.java:27) at com.nexussoftware.bibliotech.presentacion.ExcepcionesSuprimidas.conTryWithResources(ExcepcionesSuprimidas.java:71) ... 1 more
Cómo leerlo, ampliando lo que aprendiste en 06-01:
- El bloque principal es el fallo que se propagó.
- Cada bloque
Suppressed:(indentado un nivel) es un fallo que ocurrió además, típicamente al cerrar un recurso. - Cada bloque puede tener a su vez sus propios
Caused by:y sus propias suprimidas.
El orden de lectura recomendado, ahora completo:
- Clase y mensaje de la excepción principal: qué falló.
- Su último
Caused by:: la causa raíz de ese fallo. - Las
Suppressed:: qué más falló por el camino, normalmente al liberar recursos.
Y un método de utilidad que te será útil para diagnosticar:
package com.nexussoftware.bibliotech.presentacion;
/** Vuelca una excepcion con sus causas y sus suprimidas, indentadas. */
public final class InformeExcepcion {
private InformeExcepcion() { }
public static String describir(Throwable t) {
StringBuilder sb = new StringBuilder();
describir(t, sb, 0, "");
return sb.toString();
}
private static void describir(Throwable t, StringBuilder sb, int nivel, String etiqueta) {
if (t == null || nivel > 10) { // proteccion anticircular
return;
}
String sangria = " ".repeat(nivel);
sb.append(sangria).append(etiqueta)
.append(t.getClass().getSimpleName()).append(": ")
.append(t.getMessage() != null ? t.getMessage() : "(sin mensaje)")
.append('\n');
// Primer marco propio, para situar el fallo
for (StackTraceElement m : t.getStackTrace()) {
if (m.getClassName().startsWith("com.nexussoftware")) {
sb.append(sangria).append(" en ").append(m.getMethodName())
.append(" (").append(m.getFileName()).append(':')
.append(m.getLineNumber()).append(")\n");
break;
}
}
// Suprimidas: ocurrieron ADEMAS (mismo nivel logico, indentadas)
for (Throwable suprimida : t.getSuppressed()) {
describir(suprimida, sb, nivel + 1, "Suppressed: ");
}
// Causa: PROVOCO la anterior
if (t.getCause() != null && t.getCause() != t) {
describir(t.getCause(), sb, nivel + 1, "Caused by: ");
}
}
}
- Combinar con
catch y finally
catch y finallyUn try-with-resources admite catch y finally como cualquier otro try, y a diferencia del try normal, puede ir solo: no necesita ninguno de los dos.
// Legal: try-with-resources sin catch ni finally
try (RecursoTrazado r = new RecursoTrazado("A")) {
r.usar();
}
// Legal: con catch
try (RecursoTrazado r = new RecursoTrazado("A")) {
r.usar();
} catch (IllegalStateException e) {
System.out.println("Fallo: " + e.getMessage());
}
// Legal: con catch y finally
try (RecursoTrazado r = new RecursoTrazado("A")) {
r.usar();
} catch (IllegalStateException e) {
System.out.println("Fallo: " + e.getMessage());
} finally {
System.out.println("Limpieza adicional, no relacionada con recursos");
}El orden exacto de ejecución cuando están todos:
flowchart TB
A["1. Se ABREN los recursos<br/>en el orden declarado"] --> B["2. Se ejecuta el CUERPO del try"]
B --> C["3. Se CIERRAN los recursos<br/>en orden INVERSO"]
C --> D{"¿Hubo excepcion?"}
D -->|"si, y hay catch compatible"| E["4. Se ejecuta el CATCH"]
D -->|"no"| F["4. Se salta el catch"]
E --> G["5. Se ejecuta el FINALLY"]
F --> G
G --> H["6. Continua tras el bloque"]
Lo esencial: los recursos se cierran ANTES del catch y del finally. Cuando el manejador se ejecuta, ya están liberados. Compruébalo:
package com.nexussoftware.bibliotech.presentacion;
public class OrdenCompleto {
static class Recurso implements AutoCloseable {
Recurso() { System.out.println("1. abrir"); }
void usar() { throw new IllegalStateException("fallo en el cuerpo"); }
@Override public void close() { System.out.println("3. cerrar"); }
}
public static void main(String[] args) {
try (Recurso r = new Recurso()) {
System.out.println("2. cuerpo");
r.usar();
} catch (IllegalStateException e) {
System.out.println("4. catch: " + e.getMessage());
} finally {
System.out.println("5. finally");
}
System.out.println("6. despues");
}
}Salida:
¿Y para qué sirve un finally aquí, si los recursos ya se cierran solos? Para limpieza que no sea de recursos: restaurar una bandera, restituir un estado, registrar la duración de la operación. Es decir, exactamente el patrón de compensación de 06-05, que sigue siendo necesario y que ahora convive con la gestión automática de recursos.
- Recursos que NO deben cerrarse así
try-with-resources cierra el recurso siempre, y eso a veces es justo lo que no quieres.
El caso más importante: System.in.
// PROBLEMA REAL: cierra System.in para TODA la aplicacion
public int leerOpcion() {
try (Scanner scanner = new Scanner(System.in)) { // MAL
return Integer.parseInt(scanner.nextLine());
}
}
// A partir de aqui, cualquier otro Scanner sobre System.in falla:
// NoSuchElementException: No line foundScanner implementa Closeable, y su close() cierra el flujo subyacente. Si ese flujo es System.in, lo cierras para todo el proceso, no solo para tu método. El siguiente Scanner que alguien cree sobre System.in lanzará NoSuchElementException en la primera lectura, y el fallo aparecerá lejos del método culpable.
Este es exactamente el error que evitó el módulo 1 con su convención de un solo Scanner compartido, creado una vez y nunca cerrado:
// CORRECTO: un unico Scanner de instancia, sin try-with-resources
public class MenuBiblioTech {
private final Scanner scanner = new Scanner(System.in); // se crea una vez
public int leerOpcion() {
// Sin try-with-resources: System.in NO se cierra
return Integer.parseInt(scanner.nextLine().trim());
}
// No se cierra nunca: System.in lo cierra la JVM al terminar el proceso
}La regla general, que ya viste enunciada en la forma de Java 9:
Cierra lo que abres. Si el recurso te lo dieron ya abierto, o es un recurso global del proceso, no eres su dueño y no debes cerrarlo.
Otros casos en los que no se debe usar try-with-resources:
| Recurso | Por qué no |
|---|---|
System.in, System.out, System.err |
Son globales del proceso. Cerrarlos afecta a toda la aplicación |
| Un recurso recibido como parámetro | El dueño es quien lo abrió; probablemente quiera seguir usándolo |
| Un recurso que se devuelve al llamador | Lo cerrarías antes de que quien lo recibe pueda usarlo |
| Recursos de un pool (conexiones gestionadas) | Con un pool, close() normalmente devuelve la conexión al pool, así que ahí sí es correcto: comprueba la documentación |
| Un recurso que debe sobrevivir al método | Guárdalo como campo y ciérralo en el close() de tu propia clase |
El tercer caso merece una nota, porque produce un error muy sutil:
// MAL: se cierra el recurso justo antes de devolverlo
public BufferedReader abrirCatalogo(String ruta) throws IOException {
try (BufferedReader lector = new BufferedReader(new FileReader(ruta))) {
return lector; // se cierra ANTES de retornar (06-05: el finally corre primero)
}
}
// Quien lo reciba obtendra "Stream closed" en la primera lectura.Si un método devuelve un recurso, no puede cerrarlo: la responsabilidad pasa al llamador, y eso debe documentarse claramente.
- Implementar
AutoCloseable en tus clases
AutoCloseable en tus clasesTus propias clases pueden participar en try-with-resources. Es la forma idiomática de modelar cualquier cosa con un ciclo de vida abrir → usar → cerrar: una sesión, un turno de trabajo, una transacción, un bloqueo, una conexión.
El esqueleto:
public class MiRecurso implements AutoCloseable {
private boolean cerrado = false;
public MiRecurso() {
// adquirir lo que haga falta
}
public void operacion() {
comprobarAbierto(); // proteger frente al uso tras el cierre
// ...
}
private void comprobarAbierto() {
if (cerrado) {
throw new IllegalStateException("El recurso ya esta cerrado");
}
}
@Override
public void close() { // sin throws: mas comodo para quien lo use
if (cerrado) {
return; // IDEMPOTENTE: cerrar dos veces no falla
}
cerrado = true;
// liberar lo adquirido
}
}Los cuatro elementos que no deben faltar:
- Una bandera
cerrado, para saber en qué estado está. close()idempotente: llamarlo dos veces no falla ni hace el trabajo dos veces.- Comprobación en los métodos de uso, que lanza
IllegalStateExceptionsi ya está cerrado (06-03: el problema es cuándo me lo pides). close()sinthrowscomprobado si puedes evitarlo.
- Buenas prácticas en
close()
close()El contrato de close() tiene reglas que conviene respetar, porque quien use tu clase las dará por supuestas.
1. Debe ser idempotente. Closeable lo exige literalmente: "si el flujo ya está cerrado, invocar este método no tiene efecto". AutoCloseable solo lo recomienda, pero cúmplelo igual. Sin idempotencia, este código explota:
try (MiRecurso r = new MiRecurso()) {
r.operacion();
r.close(); // cierre explicito anticipado
} // ...y el try-with-resources lo cierra OTRA VEZ2. No debe lanzar si se puede evitar. Una excepción en close() es especialmente traicionera: si el cuerpo también falló, la tuya queda suprimida y probablemente nadie la lea; y si el cuerpo fue bien, tu excepción convierte una operación exitosa en un fallo. Lo razonable en la mayoría de los casos es registrar el problema y no propagarlo:
@Override
public void close() {
if (cerrado) { return; }
cerrado = true;
try {
liberarRecursoSubyacente();
} catch (RuntimeException e) {
// Se registra, no se propaga: cerrar no debe romper una operacion correcta.
// En 06-07 esto sera logger.log(Level.WARNING, "...", e).
System.err.println("Aviso: fallo al liberar el recurso: " + e.getMessage());
}
}Con una salvedad importante: si close() es el momento en que los datos se confirman, entonces sí debe lanzar. Un BufferedWriter.close() vacía el búfer al disco; si eso falla, los datos no se han escrito y quien llama tiene que enterarse. Silenciar ese fallo sería peor que propagarlo.
3. Debe ser rápido. close() se ejecuta en el camino de salida, a menudo mientras se propaga una excepción. No es sitio para operaciones largas ni para lógica de negocio pesada.
4. Debe dejar el objeto inutilizable, no medio usable. Después de close(), los métodos de uso deben lanzar IllegalStateException, no comportarse de forma extraña.
5. No dependas de finalize(). El viejo mecanismo de finalización está obsoleto desde Java 9 y marcado para eliminación: no hay ninguna garantía de cuándo —ni de si— se ejecutará. AutoCloseable con try-with-resources es el mecanismo correcto y determinista. (Cleaner es la alternativa moderna para casos avanzados, y queda fuera del alcance de este curso.)
- BiblioTech:
SesionBiblioteca y ExportadorCatalogo
SesionBiblioteca y ExportadorCatalogoPrimero, la sesión de trabajo. Un empleado abre un turno, hace operaciones, y al cerrarlo se consolidan las estadísticas y se liberan los bloqueos. Se salga como se salga del bloque.
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Turno de trabajo de un empleado sobre BiblioTech.
*
* Al cerrarse:
* 1. consolida las estadisticas del turno,
* 2. libera el bloqueo del catalogo,
* 3. deja la sesion inutilizable.
*
* Implementa AutoCloseable, no Closeable, porque no es un recurso de E/S.
* Y su close() NO declara throws: asi quien la use no esta obligado a capturar nada.
*/
public class SesionBiblioteca implements AutoCloseable {
private final String idEmpleado;
private final int diaApertura;
private final Catalogo catalogo;
private final List<String> operaciones = new ArrayList<>();
private int prestamosRealizados = 0;
private int devolucionesRealizadas = 0;
private double multasCobradas = 0.0;
private boolean cerrada = false;
/** Estadisticas acumuladas de todos los turnos (estaticas: 03-02). */
private static int turnosCompletados = 0;
private static int operacionesTotales = 0;
public SesionBiblioteca(String idEmpleado, int diaApertura, Catalogo catalogo) {
this.idEmpleado = Objects.requireNonNull(idEmpleado, "El empleado no puede ser nulo");
this.catalogo = Objects.requireNonNull(catalogo, "El catalogo no puede ser nulo");
if (diaApertura < 1) {
throw new IllegalArgumentException("El dia debe ser 1 o posterior, y era: " + diaApertura);
}
this.diaApertura = diaApertura;
catalogo.bloquear(idEmpleado); // adquisicion del recurso
System.out.println(" [sesion de " + idEmpleado + " abierta el dia " + diaApertura + "]");
}
// ---------- Operaciones del turno ----------
public void anotarPrestamo(String referencia) {
comprobarAbierta();
prestamosRealizados++;
operaciones.add("PRESTAMO " + referencia);
}
public void anotarDevolucion(String referencia, double multa) {
comprobarAbierta();
if (multa < 0) {
throw new IllegalArgumentException("La multa no puede ser negativa: " + multa);
}
devolucionesRealizadas++;
multasCobradas += multa;
operaciones.add(String.format("DEVOLUCION %s (multa %.2f)", referencia, multa));
}
public List<String> getOperaciones() {
comprobarAbierta();
return List.copyOf(operaciones);
}
/**
* Todos los metodos de uso comprueban el estado.
* Es IllegalStateException, no IllegalArgumentException: el problema
* es CUANDO se pide, no que se pide (06-03).
*/
private void comprobarAbierta() {
if (cerrada) {
throw new IllegalStateException(
"La sesion de " + idEmpleado + " ya esta cerrada");
}
}
// ---------- Cierre ----------
/**
* Consolida y libera. IDEMPOTENTE y sin throws comprobado.
*
* Se ejecuta con exito y con fallo del cuerpo, porque el try-with-resources
* lo garantiza igual que un finally (06-05).
*/
@Override
public void close() {
if (cerrada) {
return; // idempotente
}
cerrada = true;
// 1. Consolidar estadisticas
turnosCompletados++;
operacionesTotales += operaciones.size();
System.out.printf(" [sesion de %s cerrada] %d prestamos, %d devoluciones, %.2f EUR%n",
idEmpleado, prestamosRealizados, devolucionesRealizadas, multasCobradas);
// 2. Liberar el bloqueo, protegido para no romper una operacion correcta
try {
catalogo.desbloquear(idEmpleado);
} catch (RuntimeException e) {
// No se propaga: cerrar no debe convertir un turno correcto en un fallo.
// En 06-07 esto sera logger.log(Level.WARNING, ..., e).
System.err.println(" Aviso: no se pudo liberar el bloqueo del catalogo: "
+ e.getMessage());
}
}
public boolean estaCerrada() { return cerrada; }
public static int getTurnosCompletados() { return turnosCompletados; }
public static int getOperacionesTotales() { return operacionesTotales; }
}Ahora el exportador, que sí toca un fichero y sirve para ver dos recursos anidados:
package com.nexussoftware.bibliotech.servicio;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import com.nexussoftware.bibliotech.dominio.Material;
/**
* Exporta el catalogo a un fichero de texto.
*
* Implementa Closeable (no AutoCloseable) porque ES un recurso de E/S y su
* close() puede lanzar IOException legitimamente: al cerrar se vacia el
* buffer al disco, y si eso falla, los datos NO se han escrito. Ese fallo
* SI debe llegar al llamador.
*
* (La API de escritura se desarrolla en el modulo 7.)
*/
public class ExportadorCatalogo implements java.io.Closeable {
private final String ruta;
private final BufferedWriter escritor;
private int materialesEscritos = 0;
private boolean cerrado = false;
public ExportadorCatalogo(String ruta) throws IOException {
this.ruta = ruta;
this.escritor = new BufferedWriter(new FileWriter(ruta));
escribirCabecera();
}
private void escribirCabecera() throws IOException {
escritor.write("# Catalogo de BiblioTech - Nexus Software");
escritor.newLine();
escritor.write("# referencia;titulo;disponible");
escritor.newLine();
}
public void exportar(Material material) throws IOException {
if (cerrado) {
throw new IllegalStateException("El exportador de '" + ruta + "' ya esta cerrado");
}
escritor.write(material.getReferencia() + ";" + material.getTitulo() + ";"
+ material.estaDisponible());
escritor.newLine();
materialesEscritos++;
}
public void exportarTodos(List<Material> materiales) throws IOException {
for (Material m : materiales) {
exportar(m);
}
}
public int getMaterialesEscritos() { return materialesEscritos; }
/**
* Cierra el fichero.
*
* AQUI SI se propaga la IOException: el close() de un escritor vacia el
* buffer al disco. Si falla, los datos no estan guardados y quien llama
* tiene que enterarse. Es la excepcion a la regla de "close no lanza".
*/
@Override
public void close() throws IOException {
if (cerrado) {
return; // idempotente
}
cerrado = true;
try {
escritor.write("# Total: " + materialesEscritos + " materiales");
escritor.newLine();
} finally {
escritor.close(); // vacia el buffer y libera el descriptor
}
}
}Y la demostración conjunta:
package com.nexussoftware.bibliotech.presentacion;
import java.io.IOException;
import com.nexussoftware.bibliotech.dominio.Libro;
import com.nexussoftware.bibliotech.servicio.Catalogo;
import com.nexussoftware.bibliotech.servicio.ExportadorCatalogo;
import com.nexussoftware.bibliotech.servicio.SesionBiblioteca;
public class DemoSesionYExportador {
public static void main(String[] args) {
Catalogo catalogo = new Catalogo();
catalogo.registrar(new Libro("LIB-0001", "Java Efectivo", "Bloch", 2018, "978-0000000001"));
catalogo.registrar(new Libro("LIB-0002", "Patrones de Diseno", "GoF", 1994, "978-0000000002"));
catalogo.registrar(new Libro("LIB-0003", "Refactorizacion", "Fowler", 1999, "978-0000000003"));
// === 1. Turno que va bien ===
System.out.println("=== 1. Turno correcto de Marta ===");
try (SesionBiblioteca sesion = new SesionBiblioteca("EMP-001", 10, catalogo)) {
sesion.anotarPrestamo("LIB-0001");
sesion.anotarPrestamo("LIB-0002");
sesion.anotarDevolucion("LIB-0003", 1.25);
System.out.println(" Operaciones del turno: " + sesion.getOperaciones());
}
System.out.println(" Catalogo bloqueado: " + catalogo.estaBloqueado());
// === 2. Turno que falla a mitad ===
System.out.println("\n=== 2. Turno de Diego que falla ===");
try (SesionBiblioteca sesion = new SesionBiblioteca("EMP-002", 11, catalogo)) {
sesion.anotarPrestamo("LIB-0001");
sesion.anotarDevolucion("LIB-0002", -5.0); // lanza
System.out.println(" esta linea no se ejecuta");
} catch (IllegalArgumentException e) {
System.out.println(" Fallo capturado: " + e.getMessage());
}
System.out.println(" Catalogo bloqueado: " + catalogo.estaBloqueado());
System.out.println(" (la sesion se cerro y consolido igualmente)");
// === 3. Usar la sesion despues de cerrarla ===
System.out.println("\n=== 3. Uso tras el cierre ===");
SesionBiblioteca fuera;
try (SesionBiblioteca sesion = new SesionBiblioteca("EMP-003", 12, catalogo)) {
sesion.anotarPrestamo("LIB-0003");
fuera = sesion; // se guarda la referencia
}
try {
fuera.anotarPrestamo("LIB-0001");
} catch (IllegalStateException e) {
System.out.println(" " + e.getMessage());
}
System.out.println(" close() dos veces: ");
fuera.close(); // idempotente: no falla
System.out.println(" sin errores");
// === 4. Dos recursos anidados ===
System.out.println("\n=== 4. Sesion + exportador (dos recursos) ===");
try (SesionBiblioteca sesion = new SesionBiblioteca("EMP-001", 13, catalogo);
ExportadorCatalogo exportador = new ExportadorCatalogo("catalogo-export.txt")) {
exportador.exportarTodos(catalogo.listar());
sesion.anotarPrestamo("LIB-0002");
System.out.println(" Exportados: " + exportador.getMaterialesEscritos());
} catch (IOException e) {
System.out.println(" Fallo de exportacion: " + e.getMessage());
}
// Cierre en orden INVERSO: primero el exportador, luego la sesion
System.out.println("\n=== Estadisticas globales ===");
System.out.println(" Turnos completados : " + SesionBiblioteca.getTurnosCompletados());
System.out.println(" Operaciones totales: " + SesionBiblioteca.getOperacionesTotales());
}
}Salida:
=== 1. Turno correcto de Marta === [sesion de EMP-001 abierta el dia 10] Operaciones del turno: [PRESTAMO LIB-0001, PRESTAMO LIB-0002, DEVOLUCION LIB-0003 (multa 1,25)] [sesion de EMP-001 cerrada] 2 prestamos, 1 devoluciones, 1,25 EUR Catalogo bloqueado: false === 2. Turno de Diego que falla === [sesion de EMP-002 abierta el dia 11] [sesion de EMP-002 cerrada] 1 prestamos, 0 devoluciones, 0,00 EUR Fallo capturado: La multa no puede ser negativa: -5.0 Catalogo bloqueado: false (la sesion se cerro y consolido igualmente) === 3. Uso tras el cierre === [sesion de EMP-003 abierta el dia 12] [sesion de EMP-003 cerrada] 1 prestamos, 0 devoluciones, 0,00 EUR La sesion de EMP-003 ya esta cerrada close() dos veces: sin errores === 4. Sesion + exportador (dos recursos) === [sesion de EMP-001 abierta el dia 13] Exportados: 3 [sesion de EMP-001 cerrada] 1 prestamos, 0 devoluciones, 0,00 EUR === Estadisticas globales === Turnos completados : 4 Operaciones totales: 5
Lo que demuestra el caso 2 es el corazón de la lección: el turno de Diego falló a mitad, y aun así la sesión se cerró, las estadísticas se consolidaron con lo que sí se había hecho, el bloqueo se liberó y la excepción llegó al catch con su mensaje original. Sin una sola línea de finally escrita a mano.
Y el caso 4 muestra el orden de cierre inverso: el exportador —declarado el segundo— se cierra primero, y la sesión después.
- Tabla resumen: gestión manual frente a automática
| Aspecto | try-finally manual |
try-with-resources |
|---|---|---|
| Declaración de la variable | Fuera del try, inicializada a null |
Dentro de los paréntesis |
Comprobación de null |
Manual, obligatoria | Automática |
Llamada a close() |
Manual, en el finally |
Automática |
close() que lanza comprobada |
Otro try anidado |
Automático |
| Orden con varios recursos | Manual, y hay que anidar | Automático e inverso |
| Fallo al abrir el segundo recurso | El primero se queda abierto si no anidas | El primero se cierra siempre |
| Excepción del cuerpo + del cierre | La del cierre borra la original | La original se conserva; la del cierre queda suprimida |
| Líneas para dos recursos | ~20 | 2 |
| Probabilidad de hacerlo mal | Alta | Nula |
Cuándo sigue haciendo falta finally |
— | Para compensación de estado, no de recursos |
La conclusión operativa: en código nuevo, siempre try-with-resources para cualquier cosa que se cierre. El try-finally sigue siendo necesario, pero para otra cosa: restaurar estado y compensar operaciones a medias, como en 06-05.
Errores Comunes y Consejos
Envolver System.in en un try-with-resources. Cierra la entrada estándar para toda la aplicación, y el siguiente Scanner lanzará NoSuchElementException desde un sitio que no tiene la culpa. Un solo Scanner compartido, creado una vez, nunca cerrado.
Cerrar un recurso que te han pasado como parámetro. No eres su dueño. Quien te lo dio probablemente quiera seguir usándolo. Cierra lo que abres.
Devolver un recurso desde dentro del try-with-resources. Se cierra antes de retornar —el cierre corre antes que el return, igual que el finally de 06-05— y quien lo reciba obtendrá Stream closed en la primera operación.
Declarar throws Exception en tu close(). Se lo impones a todos tus usuarios en cada try-with-resources. Restringe el throws a lo que realmente lances, o a nada.
close() no idempotente. Si alguien cierra explícitamente dentro del bloque, el cierre automático lo llamará otra vez. Con la bandera cerrado y un return temprano, resuelto.
Lanzar desde close() cuando el cuerpo fue bien. Conviertes una operación correcta en un fallo. Registra y no propagues, salvo cuando close() sea el momento en que los datos se confirman —un escritor que vacía su búfer—, porque entonces el fallo es real y hay que darlo.
Buscar la excepción del cierre en getCause(). No está ahí: está en getSuppressed(). getCause() es A provocó B; getSuppressed() es A y B ocurrieron además.
Reasignar la variable del recurso. No compila, y es una buena noticia: si se pudiera, dejarías el primer recurso abierto para siempre.
Usar el recurso en el catch o en el finally del mismo try. No está en el alcance —y aunque lo estuviera, ya está cerrado.
Confiar en finalize() para liberar recursos. Está obsoleto desde Java 9 y marcado para eliminación, y no garantiza cuándo ni si se ejecuta. AutoCloseable es determinista.
Consejo: cualquier ciclo de vida abrir/usar/cerrar merece ser AutoCloseable. Sesiones, turnos, bloqueos, transacciones, temporizadores. El bloque try documenta visualmente el alcance de vida del recurso.
Consejo: elige Closeable para E/S y AutoCloseable para todo lo demás. Y en AutoCloseable, restringe el throws.
Consejo: comprueba el estado al principio de cada método de uso. Un IllegalStateException claro es infinitamente mejor que un comportamiento raro sobre un recurso ya cerrado.
Consejo: si tienes un try-finally cuyo finally solo llama a close(), conviértelo. Ganas el manejo correcto de las excepciones de cierre gratis.
Ejercicios
Ejercicio 1: BloqueoCatalogo reentrante y cerrable
Escribe BloqueoCatalogo en com.nexussoftware.bibliotech.servicio: un bloqueo de exclusión sobre el catálogo que se adquiere al construirse y se libera al cerrarse.
Requisitos:
- Implementa
AutoCloseableconclose()sinthrows. - Campos:
idPropietario,momentoAdquisicion(un contador incremental, noLocalDate),cerrado. - Un campo estático
propietarioActual(String,nullsi libre) ybloqueosConcedidos(contador). - El constructor lanza
IllegalStateExceptionsi ya hay otro propietario, con un mensaje que diga quién lo tiene. - Es reentrante: si el mismo propietario vuelve a pedirlo, se concede y se lleva la cuenta de la profundidad; el bloqueo solo se libera de verdad cuando se cierra el más externo.
close()idempotente.- Método
void comprobarPropiedad(String id)que lanceIllegalStateExceptionsiidno es el propietario actual.
En el main, demuestra: adquisición y liberación normales, reentrancia con dos niveles anidados, un intento de adquisición por otro empleado que falla, la liberación garantizada tras una excepción en el cuerpo, y el close() doble.
Ejercicio 2: excepciones suprimidas en cascada
Escribe CascadaDeFallos con una clase RecursoQueFalla implements AutoCloseable cuyo constructor reciba el nombre y dos banderas: si debe fallar al usarse y si debe fallar al cerrarse.
Escribe un main que ejecute cinco escenarios con tres recursos declarados en el mismo try-with-resources, y para cada uno muestre la excepción principal, su causa y todas sus suprimidas con getSuppressed():
- Nada falla.
- Falla solo el cuerpo.
- Falla solo el cierre del recurso central.
- Falla el cuerpo y el cierre de los tres recursos.
- Falla la apertura del tercer recurso (lanza en el constructor).
Para cada escenario, responde por escrito: ¿qué excepción se propaga?, ¿cuántas quedan suprimidas?, ¿en qué orden aparecen las suprimidas y por qué?, ¿qué recursos llegaron a cerrarse?
Añade un método volcar(Throwable t) que imprima el árbol completo de causas y suprimidas con indentación.
Ejercicio 3: ImportadorCatalogo con recursos reales
Escribe ImportadorCatalogo que lea un fichero de texto con líneas referencia;titulo;autor;anio;isbn y registre los materiales en el Catalogo, con un informe de importación.
Requisitos:
- Usa
try-with-resourcescon unBufferedReadersobre unFileReader. Documenta con un comentario que la API de E/S es el módulo 7 y limita su uso a abrir, leer líneas y cerrar. - El método
InformeImportacion importar(String ruta) throws IOExceptionno captura laIOExceptionde apertura: la declara para que decida el llamador. - Cada línea se procesa de forma independiente: los fallos de una no impiden las demás. Distingue al menos tres motivos de rechazo, uno de ellos capturando
ReferenciaDuplicadaExceptionde 06-04. - Antes de leer, el importador debe crear un fichero de ejemplo si no existe, usando
ExportadorCatalogoo unBufferedWriteren su propiotry-with-resources. - Añade un método
importarConSesion(String ruta, String idEmpleado, Catalogo catalogo)que combine dos recursos: unaSesionBibliotecay elBufferedReader, y demuestre el orden de cierre inverso. - El
maindebe demostrar: una importación correcta, una importación de un fichero inexistente (FileNotFoundExceptionpropagada y manejada arriba), y el informe final.
Soluciones
Solución 1
package com.nexussoftware.bibliotech.servicio;
import java.util.Objects;
/**
* Bloqueo de exclusion sobre el catalogo, reentrante y cerrable.
*
* Uso previsto:
* try (BloqueoCatalogo b = new BloqueoCatalogo("EMP-001")) {
* // operaciones exclusivas
* } // liberado siempre
*
* Nota: esto NO es seguro para concurrencia; los bloqueos reales entre
* hilos son materia del modulo 8. Aqui es un bloqueo logico de un solo hilo.
*/
public class BloqueoCatalogo implements AutoCloseable {
private static String propietarioActual = null;
private static int profundidad = 0;
private static int bloqueosConcedidos = 0;
private static int contadorMomentos = 0;
private final String idPropietario;
private final int momentoAdquisicion;
private final boolean esReentrada;
private boolean cerrado = false;
/**
* Adquiere el bloqueo.
*
* @throws IllegalStateException si lo tiene OTRO propietario
*/
public BloqueoCatalogo(String idPropietario) {
this.idPropietario = Objects.requireNonNull(idPropietario,
"El identificador del propietario no puede ser nulo");
if (propietarioActual != null && !propietarioActual.equals(idPropietario)) {
throw new IllegalStateException(
"El catalogo esta bloqueado por " + propietarioActual
+ "; " + idPropietario + " no puede adquirirlo");
}
this.esReentrada = (propietarioActual != null); // mismo propietario otra vez
propietarioActual = idPropietario;
profundidad++;
bloqueosConcedidos++;
this.momentoAdquisicion = ++contadorMomentos;
System.out.println(" [bloqueo " + (esReentrada ? "REENTRANTE " : "")
+ "adquirido por " + idPropietario + " (profundidad " + profundidad
+ ", momento " + momentoAdquisicion + ")]");
}
/**
* Libera el bloqueo. Solo lo libera DE VERDAD el mas externo.
* Idempotente: cerrar dos veces no hace nada la segunda.
*/
@Override
public void close() {
if (cerrado) {
return; // idempotente
}
cerrado = true;
profundidad--;
if (profundidad == 0) {
propietarioActual = null;
System.out.println(" [bloqueo LIBERADO por " + idPropietario + "]");
} else {
System.out.println(" [nivel reentrante cerrado; profundidad " + profundidad + "]");
}
}
/**
* Comprueba que quien opera es el propietario del bloqueo.
*
* @throws IllegalStateException si no lo es, o si no hay bloqueo
*/
public static void comprobarPropiedad(String id) {
if (propietarioActual == null) {
throw new IllegalStateException(
"No hay ningun bloqueo activo sobre el catalogo; " + id + " no puede operar");
}
if (!propietarioActual.equals(id)) {
throw new IllegalStateException(
"El catalogo esta bloqueado por " + propietarioActual + ", no por " + id);
}
}
public String getIdPropietario() { return idPropietario; }
public int getMomentoAdquisicion() { return momentoAdquisicion; }
public boolean estaCerrado() { return cerrado; }
public static String getPropietarioActual() { return propietarioActual; }
public static int getProfundidad() { return profundidad; }
public static int getBloqueosConcedidos() { return bloqueosConcedidos; }
// ------------------------------------------------------------------
public static void main(String[] args) {
System.out.println("=== 1. Adquisicion y liberacion normal ===");
try (BloqueoCatalogo b = new BloqueoCatalogo("EMP-001")) {
System.out.println(" operando como " + b.getIdPropietario());
comprobarPropiedad("EMP-001");
}
System.out.println(" Propietario tras cerrar: " + getPropietarioActual());
System.out.println("\n=== 2. Reentrancia de dos niveles ===");
try (BloqueoCatalogo externo = new BloqueoCatalogo("EMP-001")) {
System.out.println(" nivel externo");
try (BloqueoCatalogo interno = new BloqueoCatalogo("EMP-001")) {
System.out.println(" nivel interno");
System.out.println(" Profundidad dentro: " + getProfundidad());
}
System.out.println(" Tras cerrar el interno, profundidad: " + getProfundidad());
System.out.println(" Propietario sigue siendo: " + getPropietarioActual());
}
System.out.println(" Tras cerrar el externo: " + getPropietarioActual());
System.out.println("\n=== 3. Otro empleado intenta bloquear ===");
try (BloqueoCatalogo b = new BloqueoCatalogo("EMP-001")) {
try (BloqueoCatalogo otro = new BloqueoCatalogo("EMP-002")) {
System.out.println(" esto no deberia ocurrir");
} catch (IllegalStateException e) {
System.out.println(" Rechazado: " + e.getMessage());
}
System.out.println(" El bloqueo de EMP-001 sigue vivo: " + getPropietarioActual());
}
System.out.println("\n=== 4. Excepcion en el cuerpo ===");
try (BloqueoCatalogo b = new BloqueoCatalogo("EMP-003")) {
System.out.println(" operando...");
throw new IllegalArgumentException("fallo en mitad de la operacion");
} catch (IllegalArgumentException e) {
System.out.println(" Excepcion recibida INTACTA: " + e.getMessage());
}
System.out.println(" Propietario tras el fallo: " + getPropietarioActual()
+ " (liberado correctamente)");
System.out.println("\n=== 5. close() doble ===");
BloqueoCatalogo manual = new BloqueoCatalogo("EMP-001");
manual.close();
manual.close();
System.out.println(" Sin errores. Profundidad: " + getProfundidad());
System.out.println("\n=== 6. Operar sin bloqueo ===");
try {
comprobarPropiedad("EMP-002");
} catch (IllegalStateException e) {
System.out.println(" " + e.getMessage());
}
System.out.println("\nBloqueos concedidos en total: " + getBloqueosConcedidos());
}
}Salida:
=== 1. Adquisicion y liberacion normal ===
[bloqueo adquirido por EMP-001 (profundidad 1, momento 1)]
operando como EMP-001
[bloqueo LIBERADO por EMP-001]
Propietario tras cerrar: null
=== 2. Reentrancia de dos niveles ===
[bloqueo adquirido por EMP-001 (profundidad 1, momento 2)]
nivel externo
[bloqueo REENTRANTE adquirido por EMP-001 (profundidad 2, momento 3)]
nivel interno
Profundidad dentro: 2
[nivel reentrante cerrado; profundidad 1]
Tras cerrar el interno, profundidad: 1
Propietario sigue siendo: EMP-001
[bloqueo LIBERADO por EMP-001]
Tras cerrar el externo: null
=== 3. Otro empleado intenta bloquear ===
[bloqueo adquirido por EMP-001 (profundidad 1, momento 4)]
Rechazado: El catalogo esta bloqueado por EMP-001; EMP-002 no puede adquirirlo
El bloqueo de EMP-001 sigue vivo: EMP-001
[bloqueo LIBERADO por EMP-001]
=== 4. Excepcion en el cuerpo ===
[bloqueo adquirido por EMP-003 (profundidad 1, momento 5)]
operando...
[bloqueo LIBERADO por EMP-003]
Excepcion recibida INTACTA: fallo en mitad de la operacion
Propietario tras el fallo: null (liberado correctamente)
=== 5. close() doble ===
[bloqueo adquirido por EMP-001 (profundidad 1, momento 6)]
[bloqueo LIBERADO por EMP-001]
Sin errores. Profundidad: 0
=== 6. Operar sin bloqueo ===
No hay ningun bloqueo activo sobre el catalogo; EMP-002 no puede operar
Bloqueos concedidos en total: 6Fíjate en el caso 3: cuando la adquisición del segundo bloqueo falla en el constructor, el try-with-resources interno no llega a tener recurso que cerrar, así que la profundidad del externo no se toca. Si el constructor hubiera incrementado el contador antes de validar, ese fallo habría dejado la profundidad descuadrada para siempre. Es el fail-fast de 06-03: validar antes de modificar.
Solución 2
package com.nexussoftware.bibliotech.presentacion;
/**
* Explora el mecanismo de excepciones suprimidas con tres recursos.
*/
public class CascadaDeFallos {
static class RecursoQueFalla implements AutoCloseable {
private final String nombre;
private final boolean fallaAlUsar;
private final boolean fallaAlCerrar;
RecursoQueFalla(String nombre, boolean fallaAlUsar, boolean fallaAlCerrar) {
this(nombre, fallaAlUsar, fallaAlCerrar, false);
}
RecursoQueFalla(String nombre, boolean fallaAlUsar, boolean fallaAlCerrar,
boolean fallaAlAbrir) {
if (fallaAlAbrir) {
throw new IllegalStateException("APERTURA fallida de " + nombre);
}
this.nombre = nombre;
this.fallaAlUsar = fallaAlUsar;
this.fallaAlCerrar = fallaAlCerrar;
System.out.println(" abrir " + nombre);
}
void usar() {
System.out.println(" usar " + nombre);
if (fallaAlUsar) {
throw new IllegalStateException("USO fallido de " + nombre);
}
}
@Override
public void close() {
System.out.println(" cerrar " + nombre);
if (fallaAlCerrar) {
throw new IllegalStateException("CIERRE fallido de " + nombre);
}
}
}
public static void main(String[] args) {
escenario("1. Nada falla", false, false, false, false, false, false);
escenario("2. Falla solo el cuerpo", true, false, false, false, false, false);
escenario("3. Falla solo el cierre del central", false, false, false, false, true, false);
escenario("4. Falla el cuerpo Y los tres cierres", true, false, false, true, true, true);
escenarioAperturaFallida();
}
/**
* @param fallaCuerpo si el cuerpo lanza tras usar los recursos
* @param cierre1/2/3 si falla el cierre de cada recurso
*/
static void escenario(String titulo, boolean fallaCuerpo,
boolean usoA, boolean usoB,
boolean cierre1, boolean cierre2, boolean cierre3) {
System.out.println("\n=== " + titulo + " ===");
try (RecursoQueFalla r1 = new RecursoQueFalla("R1", usoA, cierre1);
RecursoQueFalla r2 = new RecursoQueFalla("R2", usoB, cierre2);
RecursoQueFalla r3 = new RecursoQueFalla("R3", false, cierre3)) {
r1.usar();
r2.usar();
r3.usar();
if (fallaCuerpo) {
throw new IllegalStateException("CUERPO fallido");
}
System.out.println(" (sin excepcion)");
} catch (IllegalStateException e) {
System.out.println(" --- Excepcion propagada ---");
System.out.println(volcar(e));
}
}
static void escenarioAperturaFallida() {
System.out.println("\n=== 5. Falla la APERTURA del tercer recurso ===");
try (RecursoQueFalla r1 = new RecursoQueFalla("R1", false, false);
RecursoQueFalla r2 = new RecursoQueFalla("R2", false, false);
RecursoQueFalla r3 = new RecursoQueFalla("R3", false, false, true)) {
r1.usar();
System.out.println(" esta linea NO se ejecuta");
} catch (IllegalStateException e) {
System.out.println(" --- Excepcion propagada ---");
System.out.println(volcar(e));
}
}
/** Vuelca el arbol de causas y suprimidas con indentacion. */
static String volcar(Throwable t) {
StringBuilder sb = new StringBuilder();
volcar(t, sb, 1, "");
return sb.toString();
}
private static void volcar(Throwable t, StringBuilder sb, int nivel, String etiqueta) {
if (t == null || nivel > 10) { return; }
sb.append(" ".repeat(nivel)).append(etiqueta)
.append(t.getClass().getSimpleName()).append(": ")
.append(t.getMessage()).append('\n');
for (Throwable s : t.getSuppressed()) {
volcar(s, sb, nivel + 1, "Suppressed: ");
}
if (t.getCause() != null && t.getCause() != t) {
volcar(t.getCause(), sb, nivel + 1, "Caused by: ");
}
}
}Salida (abreviada) y análisis:
=== 1. Nada falla ===
abrir R1
abrir R2
abrir R3
usar R1
usar R2
usar R3
(sin excepcion)
cerrar R3
cerrar R2
cerrar R1
=== 2. Falla solo el cuerpo ===
abrir R1 / R2 / R3 ... usar R1 / R2 / R3
cerrar R3
cerrar R2
cerrar R1
--- Excepcion propagada ---
IllegalStateException: CUERPO fallido
=== 3. Falla solo el cierre del central ===
... cerrar R3
cerrar R2
--- Excepcion propagada ---
IllegalStateException: CIERRE fallido de R2
=== 4. Falla el cuerpo Y los tres cierres ===
cerrar R3
cerrar R2
cerrar R1
--- Excepcion propagada ---
IllegalStateException: CUERPO fallido
Suppressed: IllegalStateException: CIERRE fallido de R3
Suppressed: IllegalStateException: CIERRE fallido de R2
Suppressed: IllegalStateException: CIERRE fallido de R1
=== 5. Falla la APERTURA del tercer recurso ===
abrir R1
abrir R2
cerrar R2
cerrar R1
--- Excepcion propagada ---
IllegalStateException: APERTURA fallida de R3Análisis de los cinco escenarios:
| # | Se propaga | Suprimidas | Recursos cerrados |
|---|---|---|---|
| 1 | Nada | — | R3, R2, R1 |
| 2 | La del cuerpo | 0 | R3, R2, R1 |
| 3 | La del cierre de R2 | 0 | R3, R2 (y R1 se intenta) |
| 4 | La del cuerpo | 3 | R3, R2, R1 |
| 5 | La de la apertura de R3 | 0 | R2, R1 |
Respuestas a las preguntas:
- ¿Por qué en el escenario 4 se propaga la del cuerpo? Porque el mecanismo da prioridad al fallo del cuerpo: es el que explica qué salió mal. Los fallos de cierre son consecuencias secundarias, así que se acumulan como suprimidas en lugar de sustituirla.
- ¿En qué orden aparecen las suprimidas? En el orden en que ocurren los cierres, que es el inverso al de apertura: R3, R2, R1. Cada
close()que falla añade su excepción al final del array de la principal. - Escenario 3, ¿por qué no hay suprimidas? Porque no había ninguna excepción previa cuando falló el cierre de R2, así que esa se convierte en la principal. Y ojo a un detalle: R1 también se intenta cerrar después —el mecanismo no aborta la cadena de cierres—; si R1 también hubiera fallado, su excepción habría quedado suprimida bajo la de R2.
- Escenario 5, el más instructivo. Al fallar el constructor de R3, R3 no existe y no hay nada que cerrar en él. Pero R1 y R2 sí se abrieron, y el mecanismo los cierra en orden inverso antes de propagar la excepción de apertura. El cuerpo nunca se ejecuta. Este es precisamente el caso que el patrón manual de 06-05 hacía mal salvo que anidaras un
trypor recurso.
Solución 3
package com.nexussoftware.bibliotech.servicio;
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.dominio.Libro;
import com.nexussoftware.bibliotech.dominio.ReferenciaDuplicadaException;
/**
* Importa materiales desde un fichero de texto al catalogo.
*
* NOTA SOBRE LA API DE E/S: aqui solo se usan FileReader, BufferedReader y
* BufferedWriter para abrir, leer/escribir lineas y cerrar. La entrada/salida
* completa (flujos, NIO.2, formatos) es el MODULO 7. El foco de esta clase
* esta en el CIERRE garantizado con try-with-resources.
*/
public class ImportadorCatalogo {
public record InformeImportacion(int leidas, int aceptadas, int rechazadas,
List<String> motivos) {
public String resumen() {
StringBuilder sb = new StringBuilder();
sb.append("=== INFORME DE IMPORTACION ===\n");
sb.append("Lineas leidas : ").append(leidas).append('\n');
sb.append("Aceptadas : ").append(aceptadas).append('\n');
sb.append("Rechazadas : ").append(rechazadas).append('\n');
for (String m : motivos) {
sb.append(" * ").append(m).append('\n');
}
return sb.toString();
}
}
private final Catalogo catalogo;
public ImportadorCatalogo(Catalogo catalogo) {
this.catalogo = catalogo;
}
/**
* Importa un fichero al catalogo.
*
* NO captura la IOException de apertura: la declara para que decida el
* llamador si arranca con catalogo vacio o aborta (06-03).
*/
public InformeImportacion importar(String ruta) throws IOException {
List<String> motivos = new ArrayList<>();
int leidas = 0;
int aceptadas = 0;
// try-with-resources: el lector se cierra siempre, incluso si
// el bucle lanza. Con try-finally manual harian falta 8 lineas mas.
try (BufferedReader lector = new BufferedReader(new FileReader(ruta))) {
String linea;
while ((linea = lector.readLine()) != null) {
leidas++;
if (linea.isBlank() || linea.startsWith("#")) {
continue; // vacias y comentarios: se saltan sin ruido
}
// try DENTRO del bucle: una linea mala no aborta la importacion (06-02)
try {
String[] c = linea.split(";");
Libro libro = new Libro(c[0].trim(), c[1].trim(), c[2].trim(),
Integer.parseInt(c[3].trim()), c[4].trim());
catalogo.registrar(libro);
aceptadas++;
} catch (ArrayIndexOutOfBoundsException e) {
motivos.add("Linea " + leidas + ": faltan campos -> '" + linea + "'");
} catch (NumberFormatException e) {
motivos.add("Linea " + leidas + ": anio no numerico -> " + e.getMessage());
} catch (ReferenciaDuplicadaException e) {
// Excepcion del DOMINIO (06-04): usa sus datos, no su mensaje
motivos.add("Linea " + leidas + ": " + e.getTipo() + " duplicado "
+ e.getValorDuplicado() + ", ya la usa '"
+ e.getTituloExistente() + "'");
} catch (IllegalArgumentException e) {
// Validaciones de Libro (formato de referencia, anio, ISBN)
motivos.add("Linea " + leidas + ": dato invalido -> " + e.getMessage());
}
}
}
// El lector ya esta cerrado aqui, con exito o con fallo
return new InformeImportacion(leidas, aceptadas, leidas - aceptadas, motivos);
}
/**
* Importa dentro de una sesion de trabajo: DOS recursos en el mismo
* try-with-resources, cerrados en orden inverso al de apertura.
*/
public InformeImportacion importarConSesion(String ruta, String idEmpleado, int dia)
throws IOException {
try (SesionBiblioteca sesion = new SesionBiblioteca(idEmpleado, dia, catalogo);
BufferedReader lector = new BufferedReader(new FileReader(ruta))) {
List<String> motivos = new ArrayList<>();
int leidas = 0;
int aceptadas = 0;
String linea;
while ((linea = lector.readLine()) != null) {
leidas++;
if (linea.isBlank() || linea.startsWith("#")) { continue; }
try {
String[] c = linea.split(";");
catalogo.registrar(new Libro(c[0].trim(), c[1].trim(), c[2].trim(),
Integer.parseInt(c[3].trim()), c[4].trim()));
aceptadas++;
sesion.anotarPrestamo("ALTA " + c[0].trim()); // se anota en el turno
} catch (RuntimeException e) {
motivos.add("Linea " + leidas + ": " + e.getClass().getSimpleName()
+ " - " + e.getMessage());
}
}
return new InformeImportacion(leidas, aceptadas, leidas - aceptadas, motivos);
}
// Cierre INVERSO: primero el lector, luego la sesion (que consolida
// sus estadisticas y libera el bloqueo del catalogo).
}
/** Crea un fichero de ejemplo si no existe. Otro try-with-resources. */
public static void crearFicheroDeEjemplo(String ruta) throws IOException {
File fichero = new File(ruta);
if (fichero.exists()) {
return;
}
try (BufferedWriter escritor = new BufferedWriter(new FileWriter(ruta))) {
escritor.write("# Catalogo de ejemplo de BiblioTech"); escritor.newLine();
escritor.write("# referencia;titulo;autor;anio;isbn"); escritor.newLine();
escritor.write("LIB-0001;Java Efectivo;Bloch;2018;978-0000000001"); escritor.newLine();
escritor.write("LIB-0002;Patrones de Diseno;GoF;1994;978-0000000002"); escritor.newLine();
escritor.write("LIB-0003;Refactorizacion;Fowler;1999;978-0000000003"); escritor.newLine();
escritor.write("LIB-0004;Sin autor"); escritor.newLine();
escritor.write("LIB-0005;Mal anio;Autor;mil;978-0000000005"); escritor.newLine();
escritor.write("LIB-0001;Duplicado;Otro;2020;978-0000000006"); escritor.newLine();
escritor.write("LIB-0007;Anio absurdo;Autor;1200;978-0000000007"); escritor.newLine();
}
System.out.println(" (fichero de ejemplo creado en " + ruta + ")");
}
// ------------------------------------------------------------------
public static void main(String[] args) {
String ruta = "catalogo-entrada.txt";
Catalogo catalogo = new Catalogo();
ImportadorCatalogo importador = new ImportadorCatalogo(catalogo);
System.out.println("=== 1. Preparacion ===");
try {
crearFicheroDeEjemplo(ruta);
} catch (IOException e) {
System.out.println(" No se pudo crear el fichero de ejemplo: " + e.getMessage());
return;
}
System.out.println("\n=== 2. Importacion normal ===");
try {
InformeImportacion informe = importador.importar(ruta);
System.out.print(informe.resumen());
System.out.println("Catalogo: " + catalogo.tamano() + " materiales");
} catch (IOException e) {
System.out.println(" Fallo de E/S: " + e.getMessage());
}
System.out.println("\n=== 3. Fichero inexistente ===");
try {
importador.importar("no-existe.txt");
} catch (java.io.FileNotFoundException e) {
// Subclase de IOException: se puede capturar por separado
System.out.println(" El fichero no existe. Se continua con el catalogo actual.");
System.out.println(" Detalle: " + e.getMessage());
} catch (IOException e) {
System.out.println(" Otro fallo de E/S: " + e.getMessage());
}
System.out.println("\n=== 4. Importacion dentro de una sesion (dos recursos) ===");
Catalogo otro = new Catalogo();
ImportadorCatalogo conSesion = new ImportadorCatalogo(otro);
try {
InformeImportacion informe = conSesion.importarConSesion(ruta, "EMP-001", 20);
System.out.print(informe.resumen());
} catch (IOException e) {
System.out.println(" Fallo: " + e.getMessage());
}
System.out.println("\n=== Estado final ===");
System.out.println(" Catalogo principal: " + catalogo.tamano() + " materiales");
System.out.println(" Turnos completados: " + SesionBiblioteca.getTurnosCompletados());
}
}Salida:
=== 1. Preparacion === (fichero de ejemplo creado en catalogo-entrada.txt) === 2. Importacion normal === === INFORME DE IMPORTACION === Lineas leidas : 10 Aceptadas : 3 Rechazadas : 7 * Linea 6: faltan campos -> 'LIB-0004;Sin autor' * Linea 7: anio no numerico -> For input string: "mil" * Linea 8: REFERENCIA duplicado LIB-0001, ya la usa 'Java Efectivo' * Linea 9: dato invalido -> El anio de publicacion debe estar entre 1450 y 2100, y era: 1200 Catalogo: 3 materiales === 3. Fichero inexistente === El fichero no existe. Se continua con el catalogo actual. Detalle: no-existe.txt (No such file or directory) === 4. Importacion dentro de una sesion (dos recursos) === [sesion de EMP-001 abierta el dia 20] [sesion de EMP-001 cerrada] 3 prestamos, 0 devoluciones, 0,00 EUR === INFORME DE IMPORTACION === Lineas leidas : 10 Aceptadas : 3 Rechazadas : 7 ... === Estado final === Catalogo principal: 3 materiales Turnos completados: 1
Tres puntos que resume el ejercicio:
- La
IOExceptionde apertura se declara, no se captura. El importador no sabe si un fichero ausente debe abortar la aplicación o no; esa decisión es demain, que aquí opta por continuar. Es la regla de 06-03 en acción. - Dos niveles de
trycon propósitos distintos. El externo, con recursos, garantiza el cierre. El interno, dentro del bucle, tolera líneas defectuosas. Ninguno de los dos podría hacer el trabajo del otro. - El orden de cierre inverso es visible en la salida del caso 4: el lector se cierra primero (silenciosamente) y la sesión después, imprimiendo su consolidación antes de que el informe se muestre en
main.
Conclusión
Ya dominas la gestión automática de recursos. Sabes que un try-with-resources declara sus recursos entre paréntesis y que el compilador genera por ti el try-finally completo —con la comprobación de null, el close() protegido y, sobre todo, el addSuppressed() que conserva la excepción original—, de modo que las veinte líneas de contabilidad manual del patrón anterior a Java 7 se reducen a cero y con mejor comportamiento.
Conoces las dos interfaces: Closeable (java.io, close() throws IOException, idempotencia exigida) para recursos de E/S, y AutoCloseable (java.lang, close() throws Exception) para todo lo demás; con la regla práctica de restringir el throws en tu implementación, porque cada excepción comprobada que declares en close() se la impones a todos tus usuarios en cada bloque.
Has visto el orden de cierre inverso al de apertura demostrado con trazas, y por qué es imprescindible cuando un recurso envuelve a otro. Sabes que los recursos se cierran antes del catch y del finally, que si falla la apertura del segundo recurso el primero se cierra igual —el caso que el patrón manual hacía mal casi siempre—, y que la variable del recurso es implícitamente final y no existe fuera del bloque, ambas restricciones dirigidas a impedir fugas y usos sobre recursos ya cerrados. Y conoces la forma de Java 9 que admite una variable efectivamente final ya existente, con la advertencia de propiedad que la acompaña: cierra lo que abres.
Entiendes el mecanismo de las excepciones suprimidas, que es lo que resuelve el problema que 06-05 dejó abierto: cuando fallan el cuerpo y el cierre, se propaga la del cuerpo —la que explica qué salió mal— y la del cierre queda accesible en getSuppressed(), con varias suprimidas si fallan varios cierres, en el orden inverso en que se cerraron. Sabes distinguir causa de suprimida —A provocó B frente a A y B ocurrieron además— y leer un stack trace completo con sus bloques Caused by: y Suppressed: anidados.
Sabes qué recursos no deben cerrarse así: System.in el primero —de ahí la convención del módulo 1 de un solo Scanner compartido que nunca se cierra—, los recursos recibidos como parámetro, y los que el método devuelve al llamador, que se cerrarían antes de retornar. Y sabes implementar AutoCloseable en tus clases con sus cuatro elementos: bandera de estado, close() idempotente, comprobación con IllegalStateException en cada método de uso y throws mínimo. Con las buenas prácticas del close(): idempotente, rápido, que deje el objeto inutilizable, que no lance si se puede evitar —salvo cuando el cierre es el momento en que los datos se confirman, como un escritor que vacía su búfer— y que nunca dependa del obsoleto finalize().
BiblioTech ha ganado dos clases cerrables. SesionBiblioteca modela un turno de trabajo: adquiere el bloqueo del catálogo al construirse y, al cerrarse, consolida las estadísticas del turno, libera el bloqueo y queda inutilizable, se salga del bloque como se salga. La demostración lo prueba: cuando el turno de Diego falla a mitad por una multa negativa, la sesión se cierra igual, las estadísticas se consolidan con lo que sí se hizo, el bloqueo se libera y la excepción llega al catch intacta, sin una sola línea de finally escrita a mano. Y ExportadorCatalogo implementa Closeable con un close() que sí propaga su IOException, porque al cerrar se vacía el búfer al disco y ese fallo significa que los datos no se han guardado.
Con esto, las cuatro primeras fragilidades del módulo 5 están resueltas: los datos inválidos ya no abortan el programa, los errores no se comunican con null ni false mudos, las operaciones no dejan el estado a medias y los recursos no se quedan abiertos. Queda una cosa de este módulo, y es la que convierte todo lo anterior en software de producción: decidir dónde se captura cada error y dejar de escribir avisos con System.out.println.
Eso es la lección siguiente y última del módulo, Estrategias de Manejo de Errores y Logging: dónde capturar —la regla de capturar donde se puede decidir, no donde se produce—, qué hace cada capa de BiblioTech con los errores, la frontera de errores del main y el manejador global con Thread.setDefaultUncaughtExceptionHandler; qué información debe llegar al usuario y qué solo al registro —nunca un stack trace en la cara de nadie—; errores recuperables frente a irrecuperables, degradación elegante, reintentos con límite, validación previa frente a excepción, y el patrón "objeto resultado". Y toda la parte de logging: por qué System.out.println no sirve en producción, la tabla de niveles y qué registrar en cada uno, java.util.logging en la práctica —Logger por clase, Handler de consola y de fichero, Formatter, configuración por logging.properties y el registro de una excepción con log(Level.SEVERE, msg, e) en lugar de printStackTrace()—, qué no registrar nunca, cómo se escribe un mensaje de log útil, y el ecosistema real con SLF4J y Logback que verás en 11-07.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
