JavaRush /Курсы /JAVA 25 SELF /FileVisitor — обход файловой системы и рекурсивные операц...

FileVisitor — обход файловой системы и рекурсивные операции

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

1. Введение

Давайте вспомним пример обхода с помощью Files.walk():

Path start = Paths.get("my-folder");
try (Stream<Path> stream = Files.walk(start)) {
    stream.forEach(System.out::println);
}

Это работает отлично, если нужно просто пройтись по файлам и папкам и что-то с ними сделать. Но что, если задача сложнее?

  • Нужно рекурсивно удалить папку вместе со всеми файлами и подкаталогами (а не только если она пуста).
  • Нужно рекурсивно скопировать или переместить каталог.
  • Нужно собрать статистику (например, посчитать общий размер всех файлов, сгруппировать файлы по расширениям).
  • Нужно обработать ошибки (например, если к какой-то папке нет доступа, не хочется, чтобы всё падало).

В таких случаях Stream API уже не так удобен: приходится городить try-catch в лямбдах, следить за порядком обхода (например, чтобы сначала удалять файлы, а потом папки), и код становится нечитабельным.

Для этих задач и был придуман механизм обхода дерева файловой системы с помощью FileVisitor.

2. Интерфейс FileVisitor: как он устроен

Интерфейс FileVisitor<T> — это такой «обработчик событий», который получает уведомления о каждом посещённом файле и папке при обходе файловой системы.

Когда вы вызываете Files.walkFileTree(start, visitor), Java начинает обход дерева, начиная с указанного пути, и на каждом этапе вызывает соответствующий метод вашего FileVisitor.

Основные методы интерфейса FileVisitor

Вот как выглядит интерфейс:

public interface FileVisitor<T> {
    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException;
    FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException;
    FileVisitResult visitFileFailed(T file, IOException exc) throws IOException;
    FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;
}
  • preVisitDirectory — вызывается перед входом в директорию.
  • visitFile — вызывается для каждого файла.
  • visitFileFailed — вызывается, если не удалось получить доступ к файлу.
  • postVisitDirectory — вызывается после выхода из директории (то есть после обработки всех её файлов и поддиректорий).

Каждый из этих методов возвращает значение типа FileVisitResult, которое определяет, как продолжать обход:

  • FileVisitResult.CONTINUE — продолжать обход.
  • FileVisitResult.SKIP_SUBTREE — пропустить текущую директорию и всё, что в ней.
  • FileVisitResult.SKIP_SIBLINGS — пропустить остальных «братьев» (файлы и папки на этом уровне).
  • FileVisitResult.TERMINATE — полностью прекратить обход.

Класс SimpleFileVisitor

Реализовывать все четыре метода каждый раз — утомительно, особенно если вам нужно только один или два из них. Поэтому в Java есть удобный класс-адаптер SimpleFileVisitor: он уже реализует все методы с поведением «по умолчанию» (просто CONTINUE), а вы можете переопределить только нужные.

Пример:

import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class MyVisitor extends SimpleFileVisitor<Path> {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("Файл: " + file);
        return FileVisitResult.CONTINUE;
    }
}

3. Использование Files.walkFileTree: базовый пример

Пример 1: Выводим все файлы и папки

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class TreePrinter {
    public static void main(String[] args) throws IOException {
        Path start = Paths.get("my-folder");

        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                System.out.println("Папка: " + dir);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                System.out.println("  Файл: " + file);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

Вывод:

Папка: my-folder
  Файл: my-folder/file1.txt
  Файл: my-folder/file2.txt
Папка: my-folder/subdir
  Файл: my-folder/subdir/nested.txt

Как видите, обход происходит «в глубину»: сначала заходим в папку, потом обрабатываем её файлы, потом переходим к подпапкам.

4. Пример: рекурсивное удаление директории

Одна из самых частых задач: удалить папку вместе со всем её содержимым. Если пробовать удалить папку с помощью Files.delete(path), а она не пуста — получите исключение. Нужно сначала удалить все файлы и подпапки, а потом саму папку.

Вот как это делается с помощью FileVisitor:

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class RecursiveDelete {
    public static void main(String[] args) throws IOException {
        Path dirToDelete = Paths.get("test-folder");

        Files.walkFileTree(dirToDelete, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file); // удаляем файл
                System.out.println("Удалён файл: " + file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir); // удаляем папку после того, как удалили всё внутри
                System.out.println("Удалена папка: " + dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

Важный момент:
Удаление папок происходит в postVisitDirectory, то есть после того, как удалили всё содержимое. Если бы мы попытались удалить папку до удаления файлов внутри — получили бы ошибку.

5. Пример: подсчёт общего размера всех файлов в каталоге

Давайте теперь напишем FileVisitor, который посчитает суммарный размер всех файлов в папке и её подкаталогах.

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class DirectorySizeCalculator {
    private static long totalSize = 0;

    public static void main(String[] args) throws IOException {
        Path start = Paths.get("my-folder");

        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                totalSize += attrs.size();
                return FileVisitResult.CONTINUE;
            }
        });

        System.out.println("Общий размер: " + totalSize + " байт");
    }
}

Обратите внимание:
Мы используем поле totalSize для накопления размера. В реальных приложениях лучше избегать статических полей и передавать переменные через объекты, но для простоты примера — так.

6. Пример: поиск файлов по маске (расширению) с помощью FileVisitor

Допустим, нам нужно найти все .txt файлы в каталоге и подкаталогах, и вывести их список.

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class TxtFileFinder {
    public static void main(String[] args) throws IOException {
        Path start = Paths.get("my-folder");

        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                if (file.getFileName().toString().endsWith(".txt")) {
                    System.out.println("Найден .txt файл: " + file);
                }
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

Если хочется собрать список найденных файлов, можно завести список:

import java.util.ArrayList;
import java.util.List;

// внутри main:
List<Path> txtFiles = new ArrayList<>();
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        if (file.getFileName().toString().endsWith(".txt")) {
            txtFiles.add(file);
        }
        return FileVisitResult.CONTINUE;
    }
});
System.out.println("Всего найдено: " + txtFiles.size());

7. Обработка ошибок и особенности обхода

Что делать, если к файлу или папке нет доступа?

Иногда при обходе встречаются файлы или папки, к которым нет доступа (например, если у вас нет прав, или файл занят другим процессом). В этом случае будет вызван метод visitFileFailed.

Пример:

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
    System.err.println("Ошибка доступа к файлу: " + file + " (" + exc + ")");
    return FileVisitResult.CONTINUE; // продолжаем обход несмотря на ошибку
}

Если вы хотите прервать обход при ошибке, верните TERMINATE.

Как пропустить папку целиком?

Если вы не хотите заходить в определённую папку (например, папка .git или node_modules), можно вернуть SKIP_SUBTREE в методе preVisitDirectory:

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
    if (dir.getFileName().toString().equals("node_modules")) {
        return FileVisitResult.SKIP_SUBTREE; // не заходить в эту папку и её поддиректории
    }
    return FileVisitResult.CONTINUE;
}

8. Практика: реализуем собственный FileVisitor

Давайте реализуем FileVisitor, который:

  • Находит все файлы с расширением .java в каталоге и подкаталогах,
  • Считает их количество,
  • Считает общий размер этих файлов.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class JavaFilesStats {
    public static void main(String[] args) throws IOException {
        Path start = Paths.get("src"); // например, исходный код проекта

        class JavaFileVisitor extends SimpleFileVisitor<Path> {
            int count = 0;
            long totalSize = 0;

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                if (file.getFileName().toString().endsWith(".java")) {
                    count++;
                    totalSize += attrs.size();
                    System.out.println("Найден .java файл: " + file);
                }
                return FileVisitResult.CONTINUE;
            }
        }

        JavaFileVisitor visitor = new JavaFileVisitor();
        Files.walkFileTree(start, visitor);

        System.out.println("Всего .java файлов: " + visitor.count);
        System.out.println("Общий размер: " + visitor.totalSize + " байт");
    }
}

9. Типичные ошибки при использовании FileVisitor

Ошибка № 1: попытка удалить директорию до удаления файлов внутри. Если вы вызываете Files.delete(dir) в preVisitDirectory, получите исключение — сначала нужно удалить все файлы и подпапки, только потом саму директорию (делайте это в postVisitDirectory).

Ошибка № 2: забыли обработать ошибки доступа. Если не переопределить visitFileFailed, программа может неожиданно завершиться при встрече с защищённым файлом. Лучше явно выводить ошибку и продолжать обход.

Ошибка № 3: ожидание, что операция «скрыть файл» сработает одинаково на всех ОС. На Linux и macOS скрытым считается файл, начинающийся с точки (.gitignore), а на Windows — если выставлен специальный атрибут. Не путайте эти подходы.

Ошибка № 4: использовать статические поля для накопления результата в многопоточных приложениях. Если вы запускаете несколько обходов параллельно, статические поля приведут к путанице. Лучше использовать поля экземпляра (или локального класса, как в примере выше).

Ошибка № 5: забыли закрыть ресурсы при работе с потоками внутри FileVisitor. Если ваш FileVisitor читает/записывает файлы, используйте try-with-resources, чтобы не возникло утечек.

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