1. Перебирання файлів у директорії
Отримання списку файлів: Files.list і Files.walk
У Java для роботи зі вмістом папки є два дуже корисні методи:
- Files.list(Path) — повертає потік (Stream<Path>) усіх файлів і папок верхнього рівня у заданій директорії.
- Files.walk(Path) — повертає потік усіх файлів і папок у директорії та всіх її підпапках (рекурсивно).
Приклад: виведення всіх файлів і папок у директорії 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");
// Отримуємо потік усіх файлів і папок верхнього рівня
try (var stream = Files.list(dir)) {
stream.forEach(System.out::println);
}
}
}
Якщо ви хочете пройтися всіма вкладеними папками (рекурсивно), використовуйте Files.walk:
try (var stream = Files.walk(dir)) {
stream.forEach(System.out::println);
}
Увага:
Files.walk може повернути дуже багато файлів, якщо структура велика — не запускайте це у корені диска, якщо не хочете побачити «список усього всесвіту»!
Візуалізація: різниця між list і walk
| Метод | Що повертає |
|---|---|
|
Лише вміст поточної папки |
|
Усі файли й папки, включно з вкладеними |
2. Фільтрація файлів
Однією з головних переваг роботи з потоками (Stream<Path>) є можливість фільтрувати файли за будь-якою ознакою: розширенням, назвою, розміром, датою створення тощо.
Фільтрація за розширенням
Припустімо, нам потрібні лише .txt-файли. Можна відфільтрувати їх так:
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
Фільтрація за назвою
Потрібні всі файли, ім’я яких починається з "report":
stream
.filter(path -> path.getFileName().toString().startsWith("report"))
.forEach(System.out::println);
Фільтрація за розміром
Отримаємо лише «важкі» файли (понад 1 МБ):
stream
.filter(path -> {
try {
return Files.size(path) > 1_000_000;
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
Фільтрація за датою
Наприклад, файли, змінені за останні 7 днів:
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);
Усе разом: комбінована фільтрація
Можна комбінувати фільтри «як у ресторані»: наприклад, усі .log-файли старші за місяць і понад 10 КБ:
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. Масове копіювання та видалення файлів
Масові операції — це просто: отримали потік файлів, а далі застосовуємо потрібну функцію до кожного елемента. Головне — не забути про можливі помилки (наприклад, файл використовується або вже існує).
Копіювання всіх файлів із однієї папки до іншої
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("Скопійовано: " + path.getFileName());
} catch (IOException e) {
System.out.println("Помилка копіювання " + path.getFileName() + ": " + e.getMessage());
}
});
}
Видалення всіх тимчасових файлів (*.tmp)
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".tmp"))
.forEach(path -> {
try {
Files.delete(path);
System.out.println("Видалено: " + path.getFileName());
} catch (IOException e) {
System.out.println("Помилка видалення " + path.getFileName() + ": " + e.getMessage());
}
});
}
Використання Stream API для обробки колекцій шляхів
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)); // за розміром, у порядку спадання
} catch (IOException e) {
return 0;
}
})
.limit(5) // лише 5 найбільших
.forEach(System.out::println);
}
4. Практичні завдання
Пошук і видалення файлів, старших за певну дату
Видалимо всі файли, які не змінювалися більше року:
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("Видалено: " + path.getFileName());
} catch (IOException e) {
System.out.println("Помилка видалення " + path.getFileName() + ": " + e.getMessage());
}
});
}
Масове перейменування файлів за шаблоном
Припустімо, потрібно додати до кожного .txt-файлу префікс "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("Перейменовано: " + path.getFileName() + " -> " + newPath.getFileName());
} catch (IOException e) {
System.out.println("Помилка перейменування " + path.getFileName() + ": " + e.getMessage());
}
});
}
Копіювання всіх файлів із усіх підпапок (рекурсивно)
Використаємо Files.walk, щоб перебрати всі файли в усіх підпапках:
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("Скопійовано: " + path + " -> " + targetPath);
} catch (IOException e) {
System.out.println("Помилка копіювання " + path + ": " + e.getMessage());
}
});
}
5. Важливі моменти й нюанси
Обробка помилок під час масових операцій
У масових операціях майже завжди трапляються «проблемні» файли: немає доступу, файл зайнятий, шлях надто довгий, файл уже існує тощо. Якщо не обробляти винятки всередині циклу, програма може «впасти» на першому ж збої. Тому завжди використовуйте try-catch усередині forEach, щоб «проковтнути» помилку для одного файла, але обробити решту.
Вплив рекурсії на продуктивність і стек викликів
Під час використання Files.walk для великих директорій можна отримати дуже тривалий обхід, якщо виконувати рекурсію вручну. На щастя, сам метод реалізовано ефективно й він не спричиняє переповнення стека. Але якщо ви пишете власну рекурсивну функцію для видалення/копіювання директорій, будьте обережні: для величезних вкладених структур стек може переповнитися.
Робота з великими папками
- Використовуйте Files.walk обережно: якщо в папці тисячі файлів, це може забрати багато часу й спожити багато пам’яті.
- Якщо потрібно обробити лише верхній рівень — використовуйте Files.list.
- Для пошуку файлів за маскою використовуйте фільтрацію за назвою (endsWith, matches тощо).
Приклад: «Очистити папку від тимчасових файлів»
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("Видалено: " + path);
} catch (IOException e) {
System.out.println("Помилка видалення " + path + ": " + e.getMessage());
}
});
}
Основні методи для масових операцій
| Операція | Метод/підхід | Особливості |
|---|---|---|
| Отримати всі файли | |
walk — рекурсивно, list — лише верхній рівень |
| Фільтрація за розширенням | |
Можна комбінувати з іншими фільтрами |
| Копіювання групи файлів | |
Перевіряйте існування цільової папки |
| Видалення групи файлів | |
Краще використовувати try-catch для кожної операції |
| Масове перейменування | |
Перейменування — це move з новим ім’ям |
6. Типові помилки під час масових операцій
Помилка № 1: Необроблені винятки всередині forEach.
Дуже поширена проблема: якщо не обгорнути кожну операцію в try-catch, програма «впаде» на першому ж проблемному файлі, і решта не буде оброблена.
Помилка № 2: Спроба видалити/скопіювати папку як файл (або навпаки).
Методи Files.delete і Files.copy працюють із файлами та папками по-різному. Не плутайте їх! Наприклад, спроба видалити непорожню папку стандартним методом спричинить помилку.
Помилка № 3: Неправильне формування цільового шляху під час копіювання.
Якщо формувати шлях призначення без урахування структури (наприклад, не використовувати relativize), можна перезаписати файли або отримати не ту структуру в архіві.
Помилка № 4: Відкриття занадто великої кількості файлів одночасно.
Якщо ви відкриваєте потоки для читання/запису всередині циклу, не забувайте їх закривати! Краще використовувати try-with-resources.
Помилка № 5: Рекурсивний обхід без обмеження глибини.
Під час використання Files.walk у дуже великих і глибоких папках можна отримати проблеми з продуктивністю та пам’яттю. Якщо потрібно обмежити глибину — використовуйте перевантаження з параметром глибини: Files.walk(dir, depth).
Помилка № 6: Неявне ігнорування прихованих/системних файлів.
Якщо застосунок працює з користувацькими файлами, можна випадково зачепити приховані або системні файли. Для їх фільтрації використовуйте методи Files.isHidden(path).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ