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 |
|---|---|
|
Tylko zawartość bieżącego katalogu |
|
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 | |
walk — rekurencyjnie, list — tylko najwyższy poziom |
| Filtrowanie po rozszerzeniu | |
Można łączyć z innymi filtrami |
| Kopiowanie grupy plików | |
Sprawdzaj istnienie katalogu docelowego |
| Usuwanie grupy plików | |
Lepiej używać try-catch dla każdej operacji |
| Masowa zmiana nazw | |
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).
GO TO FULL VERSION