1. Wprowadzenie
Gdy pracujesz z plikami i katalogami, często trzeba wybrać tylko określone pliki: na przykład wszystkie ".java"-pliki, wszystkie obrazy, wszystkie logi za określoną datę. W tym celu w Javie (i nie tylko) używa się globbingu (glob) i wyrażeń regularnych (regex).
- Globbing — prosty sposób opisywania wzorca nazwy pliku za pomocą znaków specjalnych (*, ?, [], {}), jak w wierszu poleceń systemu Linux lub Windows.
- Regex — potężny język wyrażeń regularnych do złożonych wzorców.
Przykłady wzorców glob
- *.java — wszystkie pliki z rozszerzeniem ".java" w bieżącym katalogu.
- **/*.java — wszystkie ".java"-pliki we wszystkich podkatalogach (podwójna gwiazdka — wyszukiwanie rekurencyjne).
- *.{png,jpg} — wszystkie pliki z rozszerzeniem ".png" lub ".jpg".
- file-??.log — pliki w rodzaju "file-01.log", "file-AB.log" (dwa dowolne znaki).
- [A-Z]*.txt — wszystkie ".txt"-pliki rozpoczynające się wielką literą.
Globbing jest prostszy niż regex i najczęściej w zupełności wystarcza do filtrowania plików.
Porównanie glob i regex
| Cecha | glob | regex |
|---|---|---|
| Prostota | Bardzo proste | Bardziej złożone, ale potężniejsze |
| Symbole | |
Pełne możliwości regex |
| Przykłady | |
|
| Rekurencyjność | |
Brak wbudowanej obsługi |
| Kiedy używać | Filtrowanie plików | Złożone sprawdzenia |
2. PathMatcher: filtrowanie plików według wzorca
W Java NIO (java.nio.file) do filtrowania plików według wzorca używa się interfejsu PathMatcher. Można go uzyskać przez FileSystems.getDefault().getPathMatcher(...).
Składnia
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java");
- "glob:*.java" — wzorzec glob.
- "regex:.*\\.java" — wzorzec wyrażenia regularnego.
Przykład: filtrowanie plików w katalogu
import java.nio.file.*;
Path dir = Paths.get("src");
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path entry : stream) {
if (matcher.matches(entry.getFileName())) {
System.out.println(entry);
}
}
}
Ważne:
- matcher.matches() zazwyczaj sprawdza tylko nazwę pliku — przekazuj entry.getFileName(), a nie pełną ścieżkę.
- Do wyszukiwania rekurencyjnego używaj Files.walk() lub Files.find().
Files.walk() — metoda NIO2 zwracająca Stream<Path> ze wszystkimi plikami i katalogami w wskazanym katalogu rekurencyjnie, w tym podkatalogi. W odróżnieniu od DirectoryStream, który pokazuje tylko zawartość jednego katalogu, Files.walk() pozwala pracować z drzewem katalogów przez Stream API.
Przykład użycia:
import java.nio.file.*;
import java.util.stream.Stream;
Path start = Paths.get("src");
try (Stream<Path> stream = Files.walk(start)) { // rekursywnie wszystkie podkatalogi
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
Przykład z regex
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("regex:.*\\.(png|jpg)");
3. Files.newDirectoryStream: filtrowanie podczas przeglądania katalogu
Metoda Files.newDirectoryStream() pozwala od razu filtrować pliki według wzorca lub za pomocą własnego filtra.
Filtrowanie według wzorca glob
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
for (Path entry : stream) {
System.out.println(entry);
}
}
Drugi parametr to wzorzec glob (bez prefiksu "glob:").
Filtrowanie za pomocą DirectoryStream.Filter
Jeśli potrzebna jest bardziej złożona logika (np. filtrowanie po rozmiarze, dacie, wykluczenie katalogów), użyj DirectoryStream.Filter<Path>:
DirectoryStream.Filter<Path> filter = path -> {
// Przykład: tylko pliki, nie katalogi, oraz bez .git i node_modules
return Files.isRegularFile(path)
&& !path.getFileName().toString().equals(".git")
&& !path.getFileName().toString().equals("node_modules");
};
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) {
for (Path entry : stream) {
System.out.println(entry);
}
}
4. Files.find: zaawansowane wyszukiwanie z BiPredicate
Jeśli trzeba wyszukiwać pliki wg złożonych warunków (np. po dacie, rozmiarze, nazwie, rekurencyjnie), użyj Files.find.
Składnia
Stream<Path> stream = Files.find(
startDir, // gdzie szukać
maxDepth, // głębokość (Integer.MAX_VALUE — rekurencyjnie)
(path, attrs) -> {
// path — ścieżka do pliku
// attrs — atrybuty pliku (rozmiar, data itp.)
return path.getFileName().toString().endsWith(".log")
&& attrs.size() > 1024; // tylko duże logi
}
);
stream.forEach(System.out::println);
- Drugi parametr — maksymalna głębokość wyszukiwania.
- Trzeci — BiPredicate<Path, BasicFileAttributes>: zwraca true, jeśli plik pasuje.
Przykład: wykluczyć katalogi .git i node_modules
Stream<Path> stream = Files.find(
Paths.get("."),
Integer.MAX_VALUE,
(path, attrs) -> {
String name = path.getFileName().toString();
// Wykluczamy katalogi .git i node_modules
if (name.equals(".git") || name.equals("node_modules")) return false;
// Tylko pliki .log o rozmiarze większym niż 1 MB
return name.endsWith(".log") && attrs.size() > 1024 * 1024;
}
);
stream.forEach(System.out::println);
5. Praktyka
Przykład 1: Znaleźć wszystkie pliki .log, z wyjątkiem .git i node_modules
Files.walk(Paths.get("."))
.filter(path -> {
String name = path.getFileName().toString();
// Wykluczamy katalogi
if (name.equals(".git") || name.equals("node_modules")) return false;
// Tylko pliki .log
return name.endsWith(".log");
})
.forEach(System.out::println);
Przykład 2: Znaleźć wszystkie obrazy utworzone po określonej dacie
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
Instant after = Instant.parse("2024-06-01T00:00:00Z");
Files.find(
Paths.get("images"),
Integer.MAX_VALUE,
(path, attrs) -> {
String name = path.getFileName().toString();
return (name.endsWith(".png") || name.endsWith(".jpg"))
&& attrs.creationTime().toInstant().isAfter(after);
}
).forEach(System.out::println);
Przykład 3: Znaleźć wszystkie duże pliki (więcej niż 10 MB), z wyjątkiem .git
Files.find(
Paths.get("."),
Integer.MAX_VALUE,
(path, attrs) -> {
String name = path.getFileName().toString();
return !name.equals(".git") && attrs.size() > 10 * 1024 * 1024;
}
).forEach(System.out::println);
6. Przydatne niuanse
Krótka ściągawka składni glob
- * — dowolna liczba dowolnych znaków (z wyjątkiem separatora /)
- ** — dowolna liczba dowolnych katalogów (działa w Javie)
- ? — dokładnie jeden dowolny znak
- [abc] — dowolny ze znaków a, b, c
- [a-z] — dowolny znak z zakresu
- {a,b,c} — dowolna z wymienionych wartości (na przykład *.{png,jpg})
Przykłady:
- *.java — wszystkie ".java"-pliki w bieżącym katalogu
- **/*.java — wszystkie ".java"-pliki we wszystkich podkatalogach
- file-??.log — pliki w rodzaju "file-01.log", "file-AB.log"
- [A-Z]*.txt — wszystkie ".txt"-pliki rozpoczynające się wielką literą
Wydajność i wycieki: zamykaj DirectoryStream!
Ważne!
- DirectoryStream i strumienie z Files.find/Files.walk — to zasoby, które trzeba zamykać.
- Używaj try-with-resources:
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
for (Path entry : stream) {
// ...
}
}
- Jeśli nie zamkniesz strumienia, może dojść do wycieku zasobów (np. „zbyt wiele otwartych plików”).
- Dla Files.find i Files.walk — koniecznie wywołuj close() lub używaj try-with-resources:
try (Stream<Path> stream = Files.find(...)) {
stream.forEach(System.out::println);
}
7. Podsumowanie i typowe błędy
Błąd nr 1: Używanie wzorca glob bez świadomości, że * nie wyszukuje rekurencyjnie. Do rekurencji użyj "**/*.java" lub Files.walk.
Błąd nr 2: Przekazywanie pełnej ścieżki do matcher.matches() — zazwyczaj należy przekazywać tylko nazwę pliku (getFileName()).
Błąd nr 3: Zapomniano zamknąć DirectoryStream lub Stream<Path> — skończy się to wyciekiem zasobów.
Błąd nr 4: Zbyt skomplikowane regex-wzorce do prostej filtracji — użyj glob, jeśli wystarczy.
Błąd nr 5: Nie wykluczono katalogów pomocniczych (".git", "node_modules") — wyszukiwanie robi się wolne i „zaśmieca się” zbędnymi plikami.
GO TO FULL VERSION