1. Introduzione
Lavorare con i file in Java (e non solo!) significa sempre lavorare con risorse esterne. Quando aprite un file, il sistema operativo assegna al vostro programma un «descrittore» — un identificatore speciale che consente di leggere e scrivere nel file. Il numero di tali descrittori è limitato: se non si chiudono i file, il programma può «mangiarsi» rapidamente tutte le risorse disponibili e cominciare a generare errori misteriosi del tipo "Too many open files".
Inoltre, se un file non viene chiuso, può rimanere bloccato per altri programmi. Per esempio, avete aperto un file in scrittura, vi siete dimenticati di chiuderlo e ora né voi né altri potete modificarlo o eliminarlo. Una sorta di «sequestro eterno di ostaggi» nel mondo dei file system.
Esempio dal mondo reale
FileInputStream fis = new FileInputStream("data.txt");
int b = fis.read();
// ... facciamo qualcosa e poi ci dimentichiamo di fis.close()
Se non si chiama il metodo close(), il file rimarrà «appeso» fino alla conclusione del programma. Nelle applicazioni grandi questo può portare a perdite di risorse e persino al crash dell’applicazione.
2. Metodo vecchio: finally + close()
Fino a Java 7 il modo classico per garantire la chiusura di un file era questo:
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// leggiamo il file
int b = fis.read();
// ...
} catch (IOException e) {
System.out.println("Errore durante la lettura del file: " + e.getMessage());
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.out.println("Errore durante la chiusura del file: " + e.getMessage());
}
}
}
Svantaggi di questo approccio
- È facile dimenticare il blocco finally e ritrovarsi con perdite di risorse.
- Tanto codice superfluo, soprattutto se i flussi sono molti.
- Se durante la chiusura si verifica un’eccezione, va catturata a parte.
- Il codice diventa ingombrante e meno leggibile.
3. Approccio moderno: try-with-resources
Per fortuna, in Java 7 è stata introdotta una sintassi che risolve questi problemi in modo elegante e automatico — try-with-resources.
Come appare
try (FileInputStream fis = new FileInputStream("data.txt")) {
int b = fis.read();
// lavoriamo con il file
} catch (IOException e) {
System.out.println("Errore durante l'uso del file: " + e.getMessage());
}
// Qui fis è già chiuso automaticamente!
Punto chiave: tutte le risorse dichiarate tra le parentesi tonde dopo try verranno chiuse automaticamente alla fine del blocco — anche se a metà è stata lanciata un’eccezione. Non serve scrivere finally, né catturare le eccezioni di chiusura a parte — Java farà tutto per voi.
Quali classi si possono usare in try-with-resources?
Qualsiasi classe che implementi l’interfaccia AutoCloseable (o il più tradizionale Closeable). Questo include quasi tutti i flussi standard di I/O: FileInputStream, FileOutputStream, BufferedReader, BufferedWriter, Scanner, PrintWriter e molti altri.
4. Sintassi di try-with-resources: dettagli ed esempi
Una risorsa
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Errore: " + e.getMessage());
}
// reader è chiuso automaticamente!
Più risorse
Si possono dichiarare più risorse separate da punto e virgola:
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
} catch (IOException e) {
System.out.println("Errore durante la copia: " + e.getMessage());
}
// entrambi i flussi sono chiusi!
Ordine di chiusura: le risorse vengono chiuse in ordine inverso rispetto alla loro dichiarazione. Prima verrà chiamato writer.close(), poi reader.close(). Questo è importante se un flusso dipende dall’altro.
Uso con classi personalizzate
Se scrivete una vostra classe che lavora con risorse, implementate semplicemente l’interfaccia AutoCloseable:
class MyResource implements AutoCloseable {
public void doSomething() {
System.out.println("Lavoriamo con la risorsa!");
}
@Override
public void close() {
System.out.println("Risorsa chiusa!");
}
}
try (MyResource res = new MyResource()) {
res.doSomething();
}
// All'uscita dal blocco verrà stampato: "Risorsa chiusa!"
5. Come funziona: schema
flowchart TD
A[Apertura della risorsa in try-with-resources] --> B{Si è verificata un'eccezione nel blocco try?}
B -- No --> C[La risorsa viene chiusa automaticamente]
B -- Sì --> D[La risorsa viene chiusa automaticamente]
D --> E[L'eccezione viene propagata]
C --> F[Il programma continua l'esecuzione]
Conclusione: indipendentemente dal fatto che si verifichi o meno un errore, la risorsa verrà sempre chiusa!
6. Esempi: riscriviamo il codice «alla maniera nuova»
Prima (stile vecchio):
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("input.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Errore: " + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("Errore durante la chiusura: " + e.getMessage());
}
}
}
Dopo (try-with-resources):
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Errore: " + e.getMessage());
}
// Tutto qui, niente finally!
7. Cosa succede se si verifica un errore durante la chiusura?
A volte l’operazione di chiusura della risorsa può lanciare un’eccezione (per esempio, se il file scompare all’improvviso). In try-with-resources tali eccezioni non vanno perse: se nel blocco try c’è già stata un’eccezione e durante la chiusura ne sorge una seconda, questa verrà aggiunta come «soppressa» (suppressed exception) alla principale. Lo si può vedere con il metodo Throwable.getSuppressed().
Esempio
try (MyResource res = new MyResource()) {
throw new IOException("Errore nel blocco try");
} catch (IOException e) {
System.out.println("Errore principale: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Eccezione soppressa: " + suppressed.getMessage());
}
}
8. Quali classi supportano try-with-resources?
È semplice: qualsiasi classe che implementi l’interfaccia AutoCloseable. Eccone solo alcune tra le standard:
| Classe | Scopo |
|---|---|
|
Lettura di byte da file |
|
Scrittura di byte su file |
|
Lettura/scrittura di testo |
|
Bufferizzazione dei flussi |
|
Scrittura di testo con formattazione |
|
Lettura di dati da file/console |
|
Serializzazione/deserializzazione |
|
Lavoro con archivi ZIP |
|
Connessioni di rete |
Se utilizzate librerie di terze parti, consultate la documentazione: se esiste un metodo close(), molto probabilmente la classe supporta try-with-resources.
9. Consigli e sfumature utili
Si possono dichiarare variabili fuori dal try (da Java 9): si possono usare risorse già dichiarate se sono final o «effectively final»:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try (reader) {
// ...
}
Funziona non solo con i file: try-with-resources è comodo per qualsiasi risorsa: connessioni di rete, database, qualunque oggetto con il metodo close().
Non ignorate le eccezioni: anche con try-with-resources ricordate di catturare e gestire le eccezioni — non è una panacea, ma solo un modo comodo per evitare perdite.
Non chiudete la risorsa manualmente dentro il try: non serve — Java farà tutto per voi! Se chiamate close() manualmente e poi termina il blocco try, si tenterà di chiudere una risorsa già chiusa. Di solito è innocuo, ma può confondere.
10. Errori tipici nell’uso di try-with-resources
Errore n. 1: avete dimenticato di usare try-with-resources. Se state ancora scrivendo finally { resource.close(); } — o siete rimasti al 2011, oppure non avete letto questa lezione! Usate la sintassi moderna.
Errore n. 2: la risorsa è dichiarata fuori dal try e dentro viene solo usata. Un codice del genere non chiuderà automaticamente la risorsa:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try {
// ... usiamo reader
} finally {
// E qui ci si è dimenticati di chiuderlo!
}
Errore n. 3: chiamate close() manualmente dentro il blocco try. Non è critico, ma è ridondante e può portare a una doppia chiusura. Fidatevi di Java.
Errore n. 4: catturate solo Exception, ignorando la specificità di IO. Meglio catturare eccezioni specifiche (FileNotFoundException, IOException) per fornire messaggi chiari all’utente.
Errore n. 5: non gestite le eccezioni soppresse. Se durante la chiusura della risorsa si verifica un errore, questo può essere «soppresso». Se analizzate gli errori, non dimenticate getSuppressed().
GO TO FULL VERSION