1. NIO2: conosciamolo più da vicino
Abbiamo già fatto conoscenza con NIO2, ma ora ripeteremo e approfondiremo le conoscenze su questa utile libreria per lavorare con file e directory.
In passato in Java c’era solo la classe File. Sapeva verificare l’esistenza di un file, creare ed eliminare file e cartelle, ottenere l’elenco dei file in una directory. Ma aveva molte limitazioni:
- lavorare con i percorsi era scomodo, soprattutto se bisognava considerare sistemi operativi diversi (C:\Users\user\file.txt su Windows e /home/user/file.txt su Linux);
- mancava un supporto adeguato per i collegamenti simbolici, i permessi e gli attributi dei file;
- le possibilità di attraversare l’albero delle directory erano limitate;
- la gestione degli errori lasciava a desiderare.
Con l’arrivo di NIO2 (New Input/Output, versione 2) in Java 7 la vita dello sviluppatore è diventata più semplice e piacevole. Ora sono a disposizione:
- la classe Path per lavorare comodamente con i percorsi di file e cartelle;
- la classe Files, che fornisce tutte le operazioni principali: lettura, scrittura, copia, eliminazione, ottenimento di informazioni sui file;
- l’interfaccia FileVisitor e metodi come Files.walk, che permettono di attraversare il file system in modo semplice e flessibile.
Perché è importante?
- Portabilità multipiattaforma: Lo stesso codice funziona su Windows, Linux, macOS, senza doversi preoccupare dei separatori (/ o \).
- Sicurezza e comodità: Più informazioni sugli errori, meno magia e meno sorprese inattese.
- Potenza: Si possono elaborare directory enormi e persino effettuare attraversamenti ricorsivi con filtraggio ed elaborazione parallela.
2. Classi principali: Path e Files
Classe Path
Path — è una rappresentazione moderna di un percorso a un file o a una cartella. Non punta necessariamente a un file realmente esistente — è semplicemente un percorso con cui è comodo lavorare.
Ottenere un Path
import java.nio.file.Path;
import java.nio.file.Paths;
Path path1 = Paths.get("file.txt"); // percorso relativo
Path path2 = Paths.get("/home/user/file.txt"); // percorso assoluto
Path path3 = Path.of("mydir", "subdir", "file.txt"); // da Java 11+
Fatto: Path è indipendente dal sistema operativo. Dimenticatevi di concatenare manualmente le stringhe con / o \!
Conversione in stringa
System.out.println(path1.toString());
Ottenere la directory padre e il nome del file
Path parent = path1.getParent(); // può essere null per percorsi relativi
Path fileName = path1.getFileName(); // solo il nome del file
Classe Files
Files — è una raccolta di metodi statici per tutte le operazioni su file e directory:
- Verifica di esistenza: Files.exists(path)
- Lettura e scrittura dei file: Files.readAllBytes(path), Files.write(path, bytes)
- Ottenimento di informazioni: Files.size(path), Files.getLastModifiedTime(path)
- Copia, eliminazione, spostamento: Files.copy, Files.delete, Files.move
Esempi:
import java.nio.file.Files;
import java.nio.file.Path;
Path path = Path.of("file.txt");
if (Files.exists(path)) {
System.out.println("Il file esiste!");
System.out.println("Dimensione: " + Files.size(path) + " byte");
System.out.println("Ultima modifica: " + Files.getLastModifiedTime(path));
} else {
System.out.println("File non trovato.");
}
3. Attraversamento del file system: Files.walk e affini
Il problema del metodo vecchio
Nel vecchio API, per attraversare tutti i file in una cartella e nelle sue sottocartelle, bisognava scrivere funzioni ricorsive, verificare manualmente dove fosse un file e dove una cartella e assicurarsi di non entrare in una ricorsione infinita. Non era solo faticoso, ma anche molto facile sbagliare.
Approccio moderno: Files.walk
Files.walk(Path start) restituisce Stream<Path> — un flusso di tutti i file e le cartelle, a partire dal percorso indicato, incluse tutte le sottodirectory. Ora l’attraversamento del file system è semplicemente lavoro con gli stream!
Esempio: stampare tutti i file e le cartelle
import java.nio.file.*;
try (var paths = Files.walk(Path.of("mydir"))) {
paths.forEach(System.out::println);
}
Qui verranno stampati tutti i percorsi: sia file sia cartelle, a partire da mydir.
Esempio: solo file (senza cartelle)
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
Il metodo Files.isRegularFile(path) restituirà true solo per i file normali (non cartelle, non symlink).
Esempio: ricerca di file per estensione
Supponiamo di dover trovare tutti i file .txt nella directory e nelle sottodirectory:
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
Esempio: calcolo della dimensione totale di tutti i file
long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
totalSize = paths.filter(Files::isRegularFile)
.mapToLong(path -> {
try {
return Files.size(path);
} catch (Exception e) {
System.err.println("Errore nella lettura della dimensione: " + path);
return 0L;
}
})
.sum();
}
System.out.println("Dimensione totale dei file: " + totalSize + " byte");
Importante!
- Il metodo Files.walk restituisce uno stream che va chiuso (implementa AutoCloseable). Perciò usiamo il try-with-resources!
- Per impostazione predefinita la profondità di attraversamento è illimitata (tutte le sottodirectory). Si può limitare la profondità: Files.walk(path, maxDepth)
4. Esercizi pratici
Esercizio 1: trovare tutte le immagini nella directory
Bisogna trovare tutti i file con estensione .jpg, .png, .gif nella cartella images e stamparne i nomi.
import java.nio.file.*;
import java.util.Set;
Set<String> extensions = Set.of(".jpg", ".png", ".gif");
try (var paths = Files.walk(Path.of("images"))) {
paths.filter(Files::isRegularFile)
.filter(path -> {
String name = path.getFileName().toString().toLowerCase();
return extensions.stream().anyMatch(name::endsWith);
})
.forEach(System.out::println);
}
Esercizio 2: copiare tutti i file .txt in un’altra cartella
import java.nio.file.*;
Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");
try (var paths = Files.walk(sourceDir)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
try {
Path relative = sourceDir.relativize(path);
Path target = destDir.resolve(relative);
Files.createDirectories(target.getParent());
Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copiato: " + path + " -> " + target);
} catch (Exception e) {
System.err.println("Errore di copia: " + path);
}
});
}
Qui preserviamo la struttura delle sottodirectory.
5. Dettagli utili
Vantaggi di NIO2
Portabilità multipiattaforma
Path gestisce autonomamente i separatori delle cartelle. Il vostro codice funzionerà allo stesso modo su Windows, Linux e macOS.
Elaborazione a stream
Metodi come Files.walk restituiscono uno stream (Stream<Path>) che si può filtrare, trasformare, raccogliere in collezioni — tutto ciò che offre lo Stream API.
Lavorare con directory grandi
Il vecchio API poteva bloccarsi se i file erano troppo numerosi (ad esempio 100 000 foto). NIO2 gestisce facilmente questi casi, perché non carica tutto in memoria in una volta sola.
Supporto per symlink, attributi e permessi di accesso
È possibile sapere se un percorso è un collegamento simbolico (Files.isSymbolicLink(path)), ottenere i permessi (Files.getPosixFilePermissions(path)), conoscere il proprietario del file e molto altro.
Confronto tra il vecchio e il nuovo API
| Operazione | API vecchio (File) | API nuovo (Path, Files) |
|---|---|---|
| Verificare l’esistenza | |
|
| Ottenere la dimensione | |
|
| Elenco dei file in una cartella | |
|
| Attraversamento ricorsivo | Ricorsione manuale | |
| Copia di un file | file.renameTo() (poco affidabile) | |
| Ottenere l’estensione | Fare il parsing della stringa | |
| Ottenere il padre | |
|
| Gestione dei permessi | Quasi per nulla | |
Caratteristiche importanti
Verifica del tipo di file
- Files.isRegularFile(path) — file normale
- Files.isDirectory(path) — cartella
- Files.isSymbolicLink(path) — symlink
Lavorare con directory grandi
- Non conviene raccogliere tutti i percorsi in una lista: lavorate con gli stream (Stream<Path>) e processateli man mano.
- Al termine, lo stream va sempre chiuso (try-with-resources).
Eccezioni
- Quasi tutti i metodi possono lanciare IOException — non dimenticate di gestire gli errori (o propagarli).
Limitare la profondità di attraversamento
try (var paths = Files.walk(Path.of("mydir"), 2)) { // solo 2 livelli
// ...
}
6. Errori tipici nell’uso di NIO2
Errore n. 1: dimenticare di chiudere lo stream di walk. Se non si usa il try-with-resources, si può incorrere in una perdita di risorse — lo stream del file system resterà aperto. Usate sempre la costruzione try (var paths = Files.walk(...)) { ... }.
Errore n. 2: non verificare che il percorso sia una directory. Se si passa a Files.walk un percorso che è un file e non una cartella, si può ottenere un comportamento inatteso o un errore.
Errore n. 3: non gestire le eccezioni. Praticamente tutti i metodi di NIO2 possono generare IOException. Non ignorate questi errori — almeno mostrate un messaggio all’utente o registrateli nel log.
Errore n. 4: confusione con i separatori dei percorsi. Se concatenate manualmente i percorsi con / o \, lo state facendo inutilmente! Usate Path.of(...) o resolve(...) — faranno la cosa giusta su qualsiasi OS.
Errore n. 5: tentare di leggere una directory enorme «in memoria». Non raccogliete tutti i percorsi in una lista, se i file sono moltissimi — lavorate con gli stream (Stream<Path>) ed elaborateli man mano che arrivano.
Errore n. 6: dimenticare la multipiattaforma. Non hardcodate percorsi assoluti in stile Windows o Unix. Usate Path e operazioni come resolve/relativize — faranno la cosa giusta su qualsiasi sistema operativo.
GO TO FULL VERSION