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).

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ