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 | |
|
| Pobranie rozmiaru | |
|
| Lista plików w folderze | |
|
| Rekursywne przejście | Ręczna rekursja | |
| Kopiowanie pliku | file.renameTo() (problematyczne) | |
| Pobranie rozszerzenia | Parsowanie łańcucha | |
| Pobranie rodzica | |
|
| Praca z uprawnieniami | Prawie wcale | |
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.
GO TO FULL VERSION