JavaRush /Курсы /JAVA 25 SELF /Массовые операции над файлами

Массовые операции над файлами

JAVA 25 SELF
40 уровень , 2 лекция
Открыта

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

Метод Что возвращает
Files.list
Только содержимое текущей папки
Files.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());
            }
        });
}

Основные методы для массовых операций

Операция Метод/подход Особенности
Получить все файлы
Files.list, Files.walk
walk — рекурсивно, list — только верхний уровень
Фильтрация по расширению
filter(path -> ...)
Можно комбинировать с другими фильтрами
Копирование группы файлов
forEach + Files.copy
Проверяйте существование целевой папки
Удаление группы файлов
forEach + Files.delete
Лучше использовать try-catch для каждой операции
Массовое переименование
forEach + Files.move
Переименование — это 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).

1
Задача
JAVA 25 SELF, 40 уровень, 2 лекция
Недоступна
Создание резервной копии критически важных данных 💾
Создание резервной копии критически важных данных 💾
1
Задача
JAVA 25 SELF, 40 уровень, 2 лекция
Недоступна
Цифровой библиотекарь: сбор текстовых сокровищ 📚
Цифровой библиотекарь: сбор текстовых сокровищ 📚
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ