CodeGym /Kursy /JAVA 25 SELF /NIO2: Files, Paths, Files.walk: przechodzenie po systemie...

NIO2: Files, Paths, Files.walk: przechodzenie po systemie plików

JAVA 25 SELF
Poziom 39, Lekcja 0
Dostępny

1. NIO2: przyjrzyjmy się bliżej

Mieliśmy już okazję poznać NIO2, ale teraz powtórzymy i pogłębimy wiedzę o tej przydatnej bibliotece do pracy z plikami i katalogami.

Dawniej w Javie była tylko klasa File. Potrafiła sprawdzać istnienie pliku, tworzyć i usuwać pliki i foldery, pobierać listę plików w katalogu. Ale miała wiele ograniczeń:

  • niewygodna praca ze ścieżkami, zwłaszcza gdy trzeba uwzględniać różne systemy operacyjne (C:\Users\user\file.txt w Windows i /home/user/file.txt w Linuksie);
  • brak porządnego wsparcia dla dowiązań symbolicznych, uprawnień i atrybutów plików;
  • ograniczone możliwości przechodzenia drzewa katalogów;
  • obsługa błędów pozostawiała wiele do życzenia.

Wraz z pojawieniem się NIO2 (New Input/Output, wersja 2) w Javie 7 życie programisty stało się prostsze i przyjemniejsze. Do dyspozycji mamy:

  • klasa Path do wygodnej pracy ze ścieżkami do plików i folderów;
  • klasa Files, która zapewnia wszystkie podstawowe operacje: odczyt, zapis, kopiowanie, usuwanie, pobieranie informacji o plikach;
  • interfejs FileVisitor i metody takie jak Files.walk, które pozwalają łatwo i elastycznie przechodzić po systemie plików.

Dlaczego to ważne?

  • Wieloplatformowość: Ten sam kod działa w Windows, Linuksie i macOS, bez zastanawiania się nad separatorami (/ lub \).
  • Bezpieczeństwo i wygoda: Więcej informacji o błędach, mniej magii i nieoczekiwanych niespodzianek.
  • Moc: Można obsługiwać ogromne katalogi, a nawet przechodzić rekurencyjnie z filtrowaniem i przetwarzaniem równoległym.

2. Kluczowe klasy: Path i Files

Klasa Path

Path — to nowoczesna reprezentacja ścieżki do pliku lub folderu. Nie musi wskazywać na rzeczywiście istniejący plik — to po prostu ścieżka, z którą wygodnie się pracuje.

Utworzenie Path

import java.nio.file.Path;
import java.nio.file.Paths;

Path path1 = Paths.get("file.txt"); // ścieżka względna
Path path2 = Paths.get("/home/user/file.txt"); // ścieżka bezwzględna
Path path3 = Path.of("mydir", "subdir", "file.txt"); // od Java 11+

Fakt: Path nie zależy od systemu operacyjnego. Zapomnij o ręcznym sklejaniu ciągów za pomocą / lub \!

Konwersja do Stringa

System.out.println(path1.toString());

Pobieranie katalogu nadrzędnego i nazwy pliku

Path parent = path1.getParent(); // może być null dla ścieżek względnych
Path fileName = path1.getFileName(); // tylko nazwa pliku

Klasa Files

Files — to zbiór metod statycznych do wszystkich operacji na plikach i katalogach:

  • Sprawdzenie istnienia: Files.exists(path)
  • Odczyt i zapis plików: Files.readAllBytes(path), Files.write(path, bytes)
  • Pobieranie informacji: Files.size(path), Files.getLastModifiedTime(path)
  • Kopiowanie, usuwanie, przenoszenie: Files.copy, Files.delete, Files.move

Przykłady:

import java.nio.file.Files;
import java.nio.file.Path;

Path path = Path.of("file.txt");
if (Files.exists(path)) {
    System.out.println("Plik istnieje!");
    System.out.println("Rozmiar: " + Files.size(path) + " bajtów");
    System.out.println("Ostatnia modyfikacja: " + Files.getLastModifiedTime(path));
} else {
    System.out.println("Plik nie znaleziony.");
}

3. Przechodzenie po systemie plików: Files.walk i pokrewne

Problem starego podejścia

W starym API, aby przejść po wszystkich plikach w folderze i jego podfolderach, trzeba było pisać funkcje rekurencyjne, ręcznie sprawdzać, gdzie jest plik, a gdzie folder, i pilnować, by nie wpaść w nieskończoną rekursję. Było to nie tylko uciążliwe, ale też bardzo łatwo było popełnić błąd.

Współczesne podejście: Files.walk

Files.walk(Path start) zwraca Stream<Path> — strumień wszystkich plików i folderów, zaczynając od wskazanej ścieżki, łącznie ze wszystkimi podkatalogami. Teraz przechodzenie po systemie plików to po prostu praca na strumieniach!

Przykład: wypisz wszystkie pliki i foldery

import java.nio.file.*;

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.forEach(System.out::println);
}

Tutaj zostaną wypisane wszystkie ścieżki: zarówno pliki, jak i foldery, począwszy od mydir.

Przykład: tylko pliki (bez folderów)

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.filter(Files::isRegularFile)
         .forEach(System.out::println);
}

Metoda Files.isRegularFile(path) zwróci true tylko dla zwykłych plików (nie folderów, nie dowiązań symbolicznych).

Przykład: wyszukiwanie plików po rozszerzeniu

Załóżmy, że musimy znaleźć wszystkie pliki .txt w katalogu i podkatalogach:

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.filter(Files::isRegularFile)
         .filter(path -> path.toString().endsWith(".txt"))
         .forEach(System.out::println);
}

Przykład: zliczenie łącznego rozmiaru wszystkich plików

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("Błąd odczytu rozmiaru: " + path);
                             return 0L;
                         }
                     })
                     .sum();
}
System.out.println("Łączny rozmiar plików: " + totalSize + " bajtów");

Ważne!

  • Metoda Files.walk zwraca strumień, który trzeba zamykać (implementuje AutoCloseable). Dlatego używaj try-with-resources!
  • Domyślnie przejście odbywa się do samego dna (wszystkie podkatalogi). Można ograniczyć głębokość: Files.walk(path, maxDepth)

4. Praktyczne zadania

Zadanie 1: znajdź wszystkie obrazki w katalogu

Trzeba znaleźć wszystkie pliki z rozszerzeniami .jpg, .png, .gif w folderze images i wypisać ich nazwy.

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

Zadanie 2: skopiuj wszystkie pliki .txt do innego folderu

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

Zachowujemy tutaj strukturę podkatalogów.

5. Przydatne szczegóły

Zalety NIO2

Wieloplatformowość

Path sam radzi sobie z separatorami katalogów. Twój kod będzie działał tak samo w Windows, Linuksie i macOS.

Przetwarzanie strumieniowe

Metody typu Files.walk zwracają strumień (Stream<Path>), który można filtrować, przekształcać, zbierać do kolekcji — wszystko, co potrafi Stream API.

Praca z dużymi katalogami

Stare API mogło „upaść”, jeśli plików jest zbyt dużo (np. 100 000 zdjęć). NIO2 radzi sobie z takimi przypadkami łatwo, ponieważ nie ładuje wszystkiego do pamięci naraz.

Obsługa dowiązań symbolicznych, atrybutów, uprawnień

Można sprawdzić, czy ścieżka jest dowiązaniem symbolicznym (Files.isSymbolicLink(path)), pobrać uprawnienia (Files.getPosixFilePermissions(path)), poznać właściciela pliku i wiele więcej.

Porównanie starego i nowego API

Operacja Stare API (File) Nowe API (Path, Files)
Sprawdzenie istnienia
file.exists()
Files.exists(path)
Pobranie rozmiaru
file.length()
Files.size(path)
Lista plików w folderze
file.listFiles()
Files.list(path)
Rekursywne przejście Ręczna rekursja
Files.walk(path)
Kopiowanie pliku file.renameTo() (problematyczne)
Files.copy(src, dest)
Pobranie rozszerzenia Parsowanie łańcucha
path.getFileName().toString()
Pobranie rodzica
file.getParentFile()
path.getParent()
Praca z uprawnieniami Prawie wcale
Files.getPosixFilePermissions(path)

Ważne cechy

Sprawdzanie typu pliku

  • Files.isRegularFile(path) — zwykły plik
  • Files.isDirectory(path) — folder
  • Files.isSymbolicLink(path) — dowiązanie symboliczne

Praca z dużymi katalogami

  • Nie zbieraj wszystkich ścieżek do listy: pracuj na strumieniach (Stream<Path>) i przetwarzaj je na bieżąco.
  • Po zakończeniu pracy strumień koniecznie zamykaj (try-with-resources).

Wyjątki

  • Prawie wszystkie metody mogą rzucać IOException — pamiętaj o obsłudze błędów (albo propaguj je wyżej).

Ograniczenie głębokości przejścia

try (var paths = Files.walk(Path.of("mydir"), 2)) { // tylko 2 poziomy
    // ...
}

6. Typowe błędy przy pracy z NIO2

Błąd nr 1: zapomniano zamknąć strumień walk. Jeśli nie użyjesz try-with-resources, może dojść do wycieku zasobów — strumień systemu plików pozostanie otwarty. Zawsze używaj konstrukcji try (var paths = Files.walk(...)) { ... }.

Błąd nr 2: nie sprawdzono, że ścieżka to katalog. Jeśli przekażesz do Files.walk ścieżkę do pliku, a nie do folderu, możesz uzyskać nieoczekiwane zachowanie lub błąd.

Błąd nr 3: brak obsługi wyjątków. Niemal wszystkie metody NIO2 mogą wyrzucić IOException. Nie zostawiaj tych błędów bez reakcji — przynajmniej wyświetl komunikat użytkownikowi albo je zaloguj.

Błąd nr 4: mieszanie separatorów ścieżek. Jeśli ręcznie sklejasz ścieżki przez / lub \, robisz to niepotrzebnie! Używaj Path.of(...) lub resolve(...) — one same zrobią to poprawnie na dowolnym systemie.

Błąd nr 5: próba wczytania ogromnego katalogu „do pamięci”. Nie zbieraj wszystkich ścieżek do listy, gdy plików jest bardzo dużo — pracuj na strumieniach (Stream<Path>) i przetwarzaj je na bieżąco.

Błąd nr 6: zapomniano o wieloplatformowości. Nie hardcoduj absolutnych ścieżek w stylu Windows lub Unix. Używaj Path oraz operacji takich jak resolve/relativize — zadziałają poprawnie na każdym systemie operacyjnym.

1
Zadanie
JAVA 25 SELF, poziom 39, lekcja 0
Niedostępne
Cyfrowy inwentaryzator: przegląd zawartości folderu
Cyfrowy inwentaryzator: przegląd zawartości folderu
1
Zadanie
JAVA 25 SELF, poziom 39, lekcja 0
Niedostępne
Poszukiwanie skarbów dewelopera: znajdź wszystkie pliki Java
Poszukiwanie skarbów dewelopera: znajdź wszystkie pliki Java
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION