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).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ