CodeGym /Corsi /JAVA 25 SELF /Operazioni di massa sui file

Operazioni di massa sui file

JAVA 25 SELF
Livello 40, Lezione 2
Disponibile

1. Iterare i file in una cartella

Ottenere l’elenco dei file: Files.list e Files.walk

In Java ci sono due metodi molto utili per lavorare con il contenuto di una cartella:

  • Files.list(Path) — restituisce uno stream (Stream<Path>) di tutti i file e cartelle di primo livello nella cartella specificata.
  • Files.walk(Path) — restituisce lo stream di tutti i file e cartelle nella cartella e in tutte le sue sottocartelle (in modo ricorsivo).

Esempio: stampa di tutti i file e cartelle nella cartella data

import java.nio.file.*;
import java.io.IOException;

public class ListFilesExample {
    public static void main(String[] args) throws IOException {
        Path dir = Paths.get("data");

        // Otteniamo lo stream di tutti i file e cartelle di primo livello
        try (var stream = Files.list(dir)) {
            stream.forEach(System.out::println);
        }
    }
}

Se vuoi attraversare tutte le sottocartelle (ricorsivamente), usa Files.walk:

try (var stream = Files.walk(dir)) {
    stream.forEach(System.out::println);
}

Attenzione:
Files.walk può restituire moltissimi file se la struttura è grande — non eseguirlo alla radice del disco se non vuoi vedere «l’elenco dell’intero universo»!

Visualizzazione: differenza tra list e walk

Metodo Cosa restituisce
Files.list
Solo il contenuto della cartella corrente
Files.walk
Tutti i file e le cartelle, incluse quelle nidificate

2. Filtraggio dei file

Uno dei principali vantaggi nel lavorare con gli stream (Stream<Path>) è la possibilità di filtrare i file per qualsiasi criterio: estensione, nome, dimensione, data di creazione, ecc.

Filtraggio per estensione

Supponiamo ci servano solo i file .txt. Possiamo filtrarli così:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(System.out::println);
}

Filtraggio per nome

Vogliamo tutti i file il cui nome inizia con "report":

stream
    .filter(path -> path.getFileName().toString().startsWith("report"))
    .forEach(System.out::println);

Filtraggio per dimensione

Otteniamo solo i file «pesanti» (maggiore di 1 MB):

stream
    .filter(path -> {
        try {
            return Files.size(path) > 1_000_000;
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

Filtraggio per data

Ad esempio, i file modificati negli ultimi 7 giorni:

import java.time.*;
import java.nio.file.attribute.*;

stream
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isAfter(Instant.now().minus(Duration.ofDays(7)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

Tutto insieme: filtraggio combinato

Puoi combinare i filtri «come al ristorante»: ad esempio, tutti i .log-file più vecchi di un mese e maggiori di 10 KB:

stream
    .filter(path -> path.toString().endsWith(".log"))
    .filter(path -> {
        try {
            return Files.size(path) > 10_000;
        } catch (IOException e) {
            return false;
        }
    })
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(30)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

3. Copia ed eliminazione di file in massa

Le operazioni in massa sono semplici: ottieni uno stream di file e poi applichi la funzione necessaria a ciascun elemento. La cosa importante — non dimenticare i possibili errori (ad esempio, file in uso o già esistente).

Copiare tutti i file da una cartella a un’altra

import java.nio.file.StandardCopyOption;

Path sourceDir = Paths.get("data");
Path targetDir = Paths.get("backup");

try (var stream = Files.list(sourceDir)) {
    stream.forEach(path -> {
        Path targetPath = targetDir.resolve(path.getFileName());
        try {
            Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("Copiato: " + path.getFileName());
        } catch (IOException e) {
            System.out.println("Errore di copia " + path.getFileName() + ": " + e.getMessage());
        }
    });
}

Eliminare tutti i file temporanei (*.tmp)

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Eliminato: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("Errore di eliminazione " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Uso dello Stream API per elaborare raccolte di percorsi

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .sorted((p1, p2) -> {
            try {
                return Long.compare(Files.size(p2), Files.size(p1)); // per dimensione, decrescente
            } catch (IOException e) {
                return 0;
            }
        })
        .limit(5) // solo i 5 più grandi
        .forEach(System.out::println);
}

4. Attività pratiche

Ricerca ed eliminazione dei file più vecchi di una certa data

Eliminiamo tutti i file che non sono stati modificati da oltre un anno:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> {
            try {
                FileTime lastModified = Files.getLastModifiedTime(path);
                return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(365)));
            } catch (IOException e) {
                return false;
            }
        })
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Eliminato: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("Errore di eliminazione " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Rinomina massiva dei file secondo un modello

Supponiamo di dover aggiungere a ogni file .txt il prefisso "old_":

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(path -> {
            Path newPath = path.resolveSibling("old_" + path.getFileName());
            try {
                Files.move(path, newPath);
                System.out.println("Rinominato: " + path.getFileName() + " -> " + newPath.getFileName());
            } catch (IOException e) {
                System.out.println("Errore di rinomina " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Copia di tutti i file da tutte le sottocartelle (ricorsivamente)

Usiamo Files.walk per iterare tutti i file in tutte le sottocartelle:

try (var stream = Files.walk(sourceDir)) {
    stream
        .filter(Files::isRegularFile)
        .forEach(path -> {
            Path relative = sourceDir.relativize(path);
            Path targetPath = targetDir.resolve(relative);
            try {
                Files.createDirectories(targetPath.getParent());
                Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
                System.out.println("Copiato: " + path + " -> " + targetPath);
            } catch (IOException e) {
                System.out.println("Errore di copia " + path + ": " + e.getMessage());
            }
        });
}

5. Aspetti importanti e particolarità

Gestione degli errori nelle operazioni in massa

Nelle operazioni in massa quasi sempre si incontrano file «problematici»: accesso negato, file in uso, percorso troppo lungo, file già esistente, ecc. Se non gestisci le eccezioni all’interno del ciclo, il programma può «andare in crash» al primo errore. Quindi usa sempre try-catch dentro forEach, per «inghiottire» l’errore per un singolo file ma continuare con gli altri.

Impatto della ricorsione sulle prestazioni e sullo stack delle chiamate

Usando Files.walk per cartelle grandi puoi ottenere attraversamenti molto lunghi, se implementi la ricorsione manualmente. Per fortuna, il metodo è implementato in modo efficiente e non provoca overflow dello stack. Ma se scrivi una tua funzione ricorsiva per eliminare/copiare cartelle, fai attenzione: per strutture enormemente annidate lo stack può «scoppiare».

Lavorare con cartelle grandi

  • Usa Files.walk con cautela: se nella cartella ci sono migliaia di file, può richiedere tempo e consumare molta memoria.
  • Se devi elaborare solo il livello superiore — usa Files.list.
  • Per cercare file per pattern usa il filtraggio per nome (endsWith, matches, ecc.).

Esempio: «Pulire la cartella dai file temporanei»

Path tempDir = Paths.get("data/tmp");

try (var stream = Files.walk(tempDir)) {
    stream
        .filter(Files::isRegularFile)
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Eliminato: " + path);
            } catch (IOException e) {
                System.out.println("Errore di eliminazione " + path + ": " + e.getMessage());
            }
        });
}

Metodi principali per le operazioni in massa

Operazione Metodo/approccio Caratteristiche
Ottenere tutti i file
Files.list, Files.walk
walk — ricorsivo, list — solo livello superiore
Filtraggio per estensione
filter(path -> ...)
Combinabile con altri filtri
Copia di un gruppo di file
forEach + Files.copy
Verifica l’esistenza della cartella di destinazione
Eliminazione di un gruppo di file
forEach + Files.delete
Meglio usare try-catch per ogni operazione
Rinomina massiva
forEach + Files.move
La rinomina è un move con un nuovo nome

6. Errori tipici nelle operazioni in massa

Errore n. 1: Eccezioni non gestite all’interno di forEach.
Un problema molto comune: se non racchiudi ogni operazione in try-catch, il programma «andrà in crash» al primo file problematico e gli altri non verranno elaborati.

Errore n. 2: Tentare di eliminare/copiare una cartella come file (o viceversa).
I metodi Files.delete e Files.copy trattano file e cartelle in modo diverso. Non confonderli! Ad esempio, tentare di eliminare una cartella non vuota con il metodo standard genererà un errore.

Errore n. 3: Costruzione errata del percorso di destinazione durante la copia.
Se costruisci il percorso di destinazione senza tenere conto della struttura (ad esempio, non usare relativize), puoi sovrascrivere file o ottenere una struttura sbagliata nell’archivio.

Errore n. 4: Aprire un numero troppo elevato di file contemporaneamente.
Se apri stream per lettura/scrittura all’interno di un ciclo, non dimenticare di chiuderli! Meglio usare try-with-resources.

Errore n. 5: Attraversamento ricorsivo senza limitare la profondità.
Usando Files.walk su cartelle molto grandi e profonde puoi incontrare problemi di prestazioni e memoria. Se devi limitare la profondità — usa la versione sovraccaricata con il parametro di profondità: Files.walk(dir, depth).

Errore n. 6: Ignorare implicitamente file nascosti/sistema.
Se l’applicazione lavora con file dell’utente, potresti toccare accidentalmente file nascosti o di sistema. Per filtrarli usa il metodo Files.isHidden(path).

1
Compito
JAVA 25 SELF, livello 40, lezione 2
Bloccato
Creazione di una copia di backup dei dati critici 💾
Creazione di una copia di backup dei dati critici 💾
1
Compito
JAVA 25 SELF, livello 40, lezione 2
Bloccato
Bibliotecario digitale: raccolta di tesori testuali 📚
Bibliotecario digitale: raccolta di tesori testuali 📚
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION