CodeGym /Corsi /JAVA 25 SELF /NIO2: Files, Paths, Files.walk: attraversamento del file ...

NIO2: Files, Paths, Files.walk: attraversamento del file system

JAVA 25 SELF
Livello 39 , Lezione 0
Disponibile

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
file.exists()
Files.exists(path)
Ottenere la dimensione
file.length()
Files.size(path)
Elenco dei file in una cartella
file.listFiles()
Files.list(path)
Attraversamento ricorsivo Ricorsione manuale
Files.walk(path)
Copia di un file file.renameTo() (poco affidabile)
Files.copy(src, dest)
Ottenere l’estensione Fare il parsing della stringa
path.getFileName().toString()
Ottenere il padre
file.getParentFile()
path.getParent()
Gestione dei permessi Quasi per nulla
Files.getPosixFilePermissions(path)

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.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION