1. Вступ
Коли ви працюєте з файлами та папками, часто потрібно відібрати лише певні файли: наприклад, усі ".java"-файли, усі зображення, усі логи за певну дату. Для цього в Java (і не тільки) використовують глобінг (glob) і регулярні вирази (regex).
- Глобінг — простий спосіб описати шаблон імені файлу за допомогою спеціальних символів (*, ?, [], {}), як у командному рядку Linux або Windows.
- Regex — потужна мова регулярних виразів для складних шаблонів.
Приклади глобінг-шаблонів
- *.java — усі файли з розширенням ".java" у поточній папці.
- **/*.java — усі ".java"-файли в усіх підпапках (подвійна зірочка — рекурсивний пошук).
- *.{png,jpg} — усі файли з розширенням ".png" або ".jpg".
- file-??.log — файли на кшталт "file-01.log", "file-AB.log" (два будь-які символи).
- [A-Z]*.txt — усі ".txt"-файли, що починаються з великої літери.
Глобінг простіший, ніж regex, і найчастіше його достатньо для фільтрації файлів.
Порівняння glob і regex
| Особливість | glob | regex |
|---|---|---|
| Простота | Дуже просто | Складніше, але потужніше |
| Символи | |
Усі можливості regex |
| Приклади | |
|
| Рекурсивність | |
Немає вбудованої підтримки |
| Коли використовувати | Фільтрація файлів | Складні перевірки |
2. PathMatcher: фільтрація файлів за маскою
У Java NIO (java.nio.file) для фільтрації файлів за маскою використовують інтерфейс PathMatcher. Його можна отримати через FileSystems.getDefault().getPathMatcher(...).
Синтаксис
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java");
- "glob:*.java" — шаблон глобінгу.
- "regex:.*\\.java" — шаблон регулярного виразу.
Приклад: фільтрація файлів у папці
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);
}
}
}
Важливо:
- matcher.matches() зазвичай перевіряє лише ім’я файлу — передавайте entry.getFileName(), а не повний шлях.
- Для рекурсивного пошуку використовуйте Files.walk() або Files.find().
Files.walk() — метод NIO2, що повертає Stream<Path> з усіма файлами та папками у вказаній директорії рекурсивно, включно з підпапками. На відміну від DirectoryStream, який показує лише вміст однієї папки, Files.walk() дає змогу працювати з деревом каталогів через Stream API.
Приклад використання:
import java.nio.file.*;
import java.util.stream.Stream;
Path start = Paths.get("src");
try (Stream<Path> stream = Files.walk(start)) { // рекурсивно всі підпапки
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
Приклад з regex
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("regex:.*\\.(png|jpg)");
3. Files.newDirectoryStream: фільтрація під час перегляду папки
Метод Files.newDirectoryStream() дає змогу відразу фільтрувати файли за маскою або за допомогою власного фільтра.
Фільтрація за glob-шаблоном
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
for (Path entry : stream) {
System.out.println(entry);
}
}
Другий параметр — це glob-шаблон (без префікса "glob:").
Фільтрація за допомогою DirectoryStream.Filter
Якщо потрібна складніша логіка (наприклад, фільтрація за розміром, датою, ігнорування папок), використовуйте DirectoryStream.Filter<Path>:
DirectoryStream.Filter<Path> filter = path -> {
// Приклад: лише файли, не папки; ігноруємо .git і 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: потужна вибірка з BiPredicate
Якщо потрібно шукати файли за складними умовами (наприклад, за датою, розміром, ім’ям, рекурсивно), використовуйте Files.find.
Синтаксис
Stream<Path> stream = Files.find(
startDir, // де шукати
maxDepth, // глибина (Integer.MAX_VALUE — рекурсивно)
(path, attrs) -> {
// path — шлях до файлу
// attrs — атрибути файлу (розмір, дата тощо)
return path.getFileName().toString().endsWith(".log")
&& attrs.size() > 1024; // лише великі логи
}
);
stream.forEach(System.out::println);
- Другий параметр — максимальна глибина пошуку.
- Третій — BiPredicate<Path, BasicFileAttributes>: повертає true, якщо файл відповідає умовам.
Приклад: ігнорувати папки .git і node_modules
Stream<Path> stream = Files.find(
Paths.get("."),
Integer.MAX_VALUE,
(path, attrs) -> {
String name = path.getFileName().toString();
// Ігноруємо папки .git і node_modules
if (name.equals(".git") || name.equals("node_modules")) return false;
// Лише .log-файли розміром більше 1 МБ
return name.endsWith(".log") && attrs.size() > 1024 * 1024;
}
);
stream.forEach(System.out::println);
5. Практика
Приклад 1: Знайти всі .log-файли, окрім .git і node_modules
Files.walk(Paths.get("."))
.filter(path -> {
String name = path.getFileName().toString();
// Ігноруємо папки
if (name.equals(".git") || name.equals("node_modules")) return false;
// Лише файли .log
return name.endsWith(".log");
})
.forEach(System.out::println);
Приклад 2: Знайти всі зображення, створені після певної дати
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);
Приклад 3: Знайти всі великі файли (понад 10 МБ), окрім .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. Корисні нюанси
Коротка шпаргалка з синтаксису glob
- * — будь-яка кількість будь-яких символів (окрім роздільника /)
- ** — будь-яка кількість будь-яких папок (у Java працює)
- ? — рівно один довільний символ
- [abc] — будь-який із символів a, b, c
- [a-z] — будь-який символ із діапазону
- {a,b,c} — будь-яке з перелічених значень (наприклад, *.{png,jpg})
Приклади:
- *.java — усі ".java"-файли у поточній папці
- **/*.java — усі ".java"-файли в усіх підпапках
- file-??.log — файли на кшталт "file-01.log", "file-AB.log"
- [A-Z]*.txt — усі ".txt"-файли, що починаються з великої літери
Продуктивність і витоки: закривайте DirectoryStream!
Важливо!
- DirectoryStream і потоки з Files.find/Files.walk — це ресурси, які потрібно закривати.
- Використовуйте try-with-resources:
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
for (Path entry : stream) {
// ...
}
}
- Якщо не закривати потік, може виникнути витік ресурсів (наприклад, «занадто багато відкритих файлів»).
- Для Files.find і Files.walk — обов’язково викликайте close() або використовуйте try-with-resources:
try (Stream<Path> stream = Files.find(...)) {
stream.forEach(System.out::println);
}
7. Підсумки та типові помилки
Помилка № 1: Використання glob-шаблону без розуміння, що * не шукає рекурсивно. Для рекурсії використовуйте "**/*.java" або Files.walk.
Помилка № 2: Передача повного шляху в matcher.matches() — зазвичай потрібно передавати лише ім’я файлу (getFileName()).
Помилка № 3: Забули закрити DirectoryStream або Stream<Path> — отримаєте витік ресурсів.
Помилка № 4: Надто складні regex-шаблони для простої фільтрації — використовуйте glob, якщо цього достатньо.
Помилка № 5: Не виключили службові папки (".git", "node_modules") — пошук стає повільним і «засмічується» зайвими файлами.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ