CodeGym /Kursy /JAVA 25 SELF /Masowe operacje na plikach

Masowe operacje na plikach

JAVA 25 SELF
Poziom 40, Lekcja 2
Dostępny

1. Przegląd plików w katalogu

Pobieranie listy plików: Files.list i Files.walk

W Javie do pracy z zawartością katalogu istnieją dwie bardzo przydatne metody:

  • Files.list(Path) — zwraca strumień (Stream<Path>) wszystkich plików i folderów najwyższego poziomu w podanym katalogu.
  • Files.walk(Path) — zwraca strumień wszystkich plików i folderów w katalogu oraz we wszystkich jego podkatalogach (rekurencyjnie).

Przykład: wypisanie wszystkich plików i folderów w katalogu 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");

        // Pobieramy strumień wszystkich plików i katalogów najwyższego poziomu
        try (var stream = Files.list(dir)) {
            stream.forEach(System.out::println);
        }
    }
}

Jeśli chcesz przejść przez wszystkie zagnieżdżone foldery (rekurencyjnie), użyj Files.walk:

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

Uwaga:
Files.walk może zwrócić bardzo dużo plików, jeśli struktura jest duża — nie uruchamiaj tego w katalogu głównym dysku, jeśli nie chcesz zobaczyć „listy całego wszechświata”!

Wizualizacja: różnica między list i walk

Metoda Co zwraca
Files.list
Tylko zawartość bieżącego katalogu
Files.walk
Wszystkie pliki i foldery, w tym zagnieżdżone

2. Filtrowanie plików

Jedną z głównych zalet pracy ze strumieniami (Stream<Path>) jest możliwość filtrowania plików według dowolnego kryterium: rozszerzenia, nazwy, rozmiaru, daty utworzenia itd.

Filtrowanie po rozszerzeniu

Załóżmy, że potrzebujemy tylko plików .txt. Można je odfiltrować tak:

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

Filtrowanie po nazwie

Chcemy wszystkie pliki, których nazwa zaczyna się od "report":

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

Filtrowanie po rozmiarze

Pobierzmy tylko „duże” pliki (większe niż 1 MB):

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

Filtrowanie według daty

Na przykład pliki zmienione w ciągu ostatnich 7 dni:

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);

Wszystko razem: filtrowanie łączone

Można łączyć filtry „jak w restauracji”: na przykład wszystkie .log-pliki starsze niż miesiąc i większe niż 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. Masowe kopiowanie i usuwanie plików

Operacje masowe są proste: pobieramy strumień plików, a następnie stosujemy potrzebną funkcję do każdego elementu. Najważniejsze — pamiętać o możliwych błędach (np. plik jest używany albo już istnieje).

Kopiowanie wszystkich plików z jednego katalogu do innego

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("Skopiowano: " + path.getFileName());
        } catch (IOException e) {
            System.out.println("Błąd kopiowania " + path.getFileName() + ": " + e.getMessage());
        }
    });
}

Usuwanie wszystkich plików tymczasowych (*.tmp)

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Usunięto: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("Błąd usuwania " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Użycie Stream API do przetwarzania kolekcji ścieżek

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)); // według rozmiaru, malejąco
            } catch (IOException e) {
                return 0;
            }
        })
        .limit(5) // tylko 5 największych
        .forEach(System.out::println);
}

4. Zadania praktyczne

Wyszukiwanie i usuwanie plików starszych niż podana data

Usuńmy wszystkie pliki, które nie były modyfikowane od ponad roku:

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("Usunięto: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("Błąd usuwania " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Masowa zmiana nazw plików według wzorca

Załóżmy, że trzeba dodać do każdego .txt-pliku prefiks "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("Zmieniono nazwę: " + path.getFileName() + " -> " + newPath.getFileName());
            } catch (IOException e) {
                System.out.println("Błąd zmiany nazwy " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Kopiowanie wszystkich plików ze wszystkich podkatalogów (rekurencyjnie)

Użyjmy Files.walk, aby przejść przez wszystkie pliki we wszystkich podkatalogach:

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("Skopiowano: " + path + " -> " + targetPath);
            } catch (IOException e) {
                System.out.println("Błąd kopiowania " + path + ": " + e.getMessage());
            }
        });
}

5. Ważne kwestie i niuanse

Obsługa błędów przy operacjach masowych

Przy operacjach masowych niemal zawsze trafiają się „problematyczne” pliki: brak dostępu, plik zajęty, zbyt długa ścieżka, plik już istnieje itd. Jeśli nie obsłużysz wyjątków wewnątrz pętli, program może „wywalić się” już na pierwszym błędzie. Dlatego zawsze używaj try-catch wewnątrz forEach, aby „przełknąć” błąd dla jednego pliku, ale przetworzyć pozostałe.

Wpływ rekurencji na wydajność i stos wywołań

Przy użyciu Files.walk dla dużych katalogów można uzyskać bardzo długie przejście, jeśli rekurencję realizujesz ręcznie. Na szczęście samą metodę zaimplementowano wydajnie i nie powoduje przepełnienia stosu. Jednak jeśli piszesz własną rekurencyjną funkcję do usuwania/kopiowania katalogów, zachowaj ostrożność: dla ogromnych zagnieżdżonych struktur stos może się przepełnić.

Praca z dużymi katalogami

  • Używaj Files.walk ostrożnie: jeśli w katalogu są tysiące plików, może to zająć dużo czasu i „zjeść” sporo pamięci.
  • Jeśli trzeba przetworzyć tylko najwyższy poziom — użyj Files.list.
  • Do wyszukiwania plików według maski używaj filtrowania po nazwie (endsWith, matches itd.).

Przykład: „Wyczyścić katalog z plików tymczasowych”

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("Usunięto: " + path);
            } catch (IOException e) {
                System.out.println("Błąd usuwania " + path + ": " + e.getMessage());
            }
        });
}

Podstawowe metody do operacji masowych

Operacja Metoda/podejście Cechy
Pobrać wszystkie pliki
Files.list, Files.walk
walk — rekurencyjnie, list — tylko najwyższy poziom
Filtrowanie po rozszerzeniu
filter(path -> ...)
Można łączyć z innymi filtrami
Kopiowanie grupy plików
forEach + Files.copy
Sprawdzaj istnienie katalogu docelowego
Usuwanie grupy plików
forEach + Files.delete
Lepiej używać try-catch dla każdej operacji
Masowa zmiana nazw
forEach + Files.move
Zmiana nazwy to move z nową nazwą

6. Typowe błędy przy operacjach masowych

Błąd nr 1: Nieobsłużone wyjątki wewnątrz forEach.
Bardzo częsty problem: jeśli nie opakujesz każdej operacji w try-catch, program „wywali się” na pierwszym problematycznym pliku i pozostałe nie zostaną przetworzone.

Błąd nr 2: Próba usunięcia/skopiowania folderu jako pliku (lub odwrotnie).
Metody Files.delete i Files.copy działają inaczej dla plików i folderów. Nie myl ich! Na przykład próba usunięcia niepustego folderu standardową metodą spowoduje błąd.

Błąd nr 3: Nieprawidłowe tworzenie ścieżki docelowej przy kopiowaniu.
Jeśli tworzysz ścieżkę docelową bez uwzględniania struktury (np. nie używasz relativize), możesz nadpisać pliki lub uzyskać nieprawidłową strukturę w archiwum.

Błąd nr 4: Otwarcie zbyt dużej liczby plików jednocześnie.
Jeśli otwierasz strumienie do odczytu/zapisu wewnątrz pętli, nie zapominaj ich zamykać! Lepiej używać try-with-resources.

Błąd nr 5: Rekurencyjne przechodzenie bez ograniczenia głębokości.
Przy użyciu Files.walk dla bardzo dużych i głębokich katalogów możesz napotkać problemy z wydajnością i pamięcią. Jeśli trzeba ograniczyć głębokość — użyj przeciążenia z parametrem głębokości: Files.walk(dir, depth).

Błąd nr 6: Nieświadome operowanie na plikach ukrytych/systemowych.
Jeśli aplikacja pracuje na plikach użytkownika, możesz przypadkowo objąć operacjami pliki ukryte lub systemowe. Do ich filtrowania używaj metody Files.isHidden(path).

1
Zadanie
JAVA 25 SELF, poziom 40, lekcja 2
Niedostępne
Tworzenie kopii zapasowej krytycznych danych 💾
Tworzenie kopii zapasowej krytycznych danych 💾
1
Zadanie
JAVA 25 SELF, poziom 40, lekcja 2
Niedostępne
Cyfrowy bibliotekarz: zbieranie tekstowych skarbów 📚
Cyfrowy bibliotekarz: zbieranie tekstowych skarbów 📚
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION