JavaRush /Курси /JAVA 25 SELF /NIO2: Files, Paths, Files.walk: обхід файлової системи

NIO2: Files, Paths, Files.walk: обхід файлової системи

JAVA 25 SELF
Рівень 39 , Лекція 0
Відкрита

1. NIO2: знайомимося детальніше

Ми з вами вже встигли познайомитися з NIO2, але зараз повторимо й поглибимо знання про цю корисну бібліотеку для роботи з файлами та директоріями.

Раніше в Java був лише клас File. Він умів перевіряти існування файлу, створювати й видаляти файли та папки, отримувати список файлів у директорії. Але в нього було багато обмежень:

  • незручно працювати зі шляхами, особливо якщо потрібно враховувати різні операційні системи (C:\Users\user\file.txt на Windows і /home/user/file.txt на Linux);
  • відсутня повноцінна підтримка символьних посилань, прав доступу та атрибутів файлів;
  • можливості обходу дерева директорій були обмежені;
  • обробка помилок залишала бажати кращого.

З появою NIO2 (New Input/Output, версія 2) у Java 7 життя розробника стало простішим і приємнішим. Тепер у вашому розпорядженні є:

  • клас Path для зручної роботи зі шляхами до файлів і папок;
  • клас Files, який забезпечує всі основні операції: читання, запис, копіювання, видалення, отримання інформації про файли;
  • інтерфейс FileVisitor і методи на кшталт Files.walk, які дають змогу легко та гнучко обходити файлову систему.

Чому це важливо?

  • Кросплатформеність: Один і той самий код працює на Windows, Linux, macOS, без клопоту з розділювачами (/ або \).
  • Безпека й зручність: Більше інформації про помилки, менше «магії» та несподіваних сюрпризів.
  • Потужність: Можна обробляти величезні директорії й навіть робити обхід рекурсивно з фільтрацією та паралельною обробкою.

2. Основні класи: Path і Files

Клас Path

Path — це сучасне подання шляху до файлу або папки. Він не обов’язково вказує на реально наявний файл — це просто шлях, з яким зручно працювати.

Отримання Path

import java.nio.file.Path;
import java.nio.file.Paths;

Path path1 = Paths.get("file.txt"); // відносний шлях
Path path2 = Paths.get("/home/user/file.txt"); // абсолютний шлях
Path path3 = Path.of("mydir", "subdir", "file.txt"); // починаючи з Java 11+

Факт: Path не залежить від операційної системи. Забудьте про ручне склеювання рядків із / або \!

Перетворення на рядок

System.out.println(path1.toString());

Отримання батьківської директорії та імені файлу

Path parent = path1.getParent(); // може бути null для відносних шляхів
Path fileName = path1.getFileName(); // лише ім’я файлу

Клас Files

Files — це збірка статичних методів для всіх операцій із файлами та директоріями:

  • Перевірка існування: Files.exists(path)
  • Читання і запис файлів: Files.readAllBytes(path), Files.write(path, bytes)
  • Отримання інформації: Files.size(path), Files.getLastModifiedTime(path)
  • Копіювання, видалення, переміщення: Files.copy, Files.delete, Files.move

Приклади:

import java.nio.file.Files;
import java.nio.file.Path;

Path path = Path.of("file.txt");
if (Files.exists(path)) {
    System.out.println("Файл існує!");
    System.out.println("Розмір: " + Files.size(path) + " байт");
    System.out.println("Остання зміна: " + Files.getLastModifiedTime(path));
} else {
    System.out.println("Файл не знайдено.");
}

3. Обхід файлової системи: Files.walk і друзі

Проблема старого способу

У старому API, щоб обійти всі файли в папці та її підпапках, доводилося писати рекурсивні функції, вручну перевіряти, де файл, а де папка, і стежити, щоб не потрапити в нескінченну рекурсію. Це було не лише виснажливо, а й легко помилитися.

Сучасний спосіб: Files.walk

Files.walk(Path start) повертає Stream<Path> — потік усіх файлів і папок, починаючи із зазначеної директорії, включно з усіма підкаталогами. Тепер обхід файлової системи — це просто робота з потоками!

Приклад: Вивести всі файли та папки

import java.nio.file.*;

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.forEach(System.out::println);
}

Тут будуть виведені всі шляхи: і файли, і папки, починаючи з mydir.

Приклад: Лише файли (без папок)

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.filter(Files::isRegularFile)
         .forEach(System.out::println);
}

Метод Files.isRegularFile(path) повертає true лише для звичайних файлів (ні папок, ні симлінків).

Приклад: Пошук файлів за розширенням

Припустімо, нам потрібно знайти всі файли .txt у каталозі та підкаталогах:

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.filter(Files::isRegularFile)
         .filter(path -> path.toString().endsWith(".txt"))
         .forEach(System.out::println);
}

Приклад: Підрахунок загального розміру всіх файлів

long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
    totalSize = paths.filter(Files::isRegularFile)
                     .mapToLong(path -> {
                         try {
                             return Files.size(path);
                         } catch (Exception e) {
                             System.err.println("Помилка читання розміру: " + path);
                             return 0L;
                         }
                     })
                     .sum();
}
System.out.println("Загальний розмір файлів: " + totalSize + " байт");

Важливо!

  • Метод Files.walk повертає потік, який потрібно закривати (він реалізує AutoCloseable). Тому використовуємо try-with-resources!
  • За замовчуванням глибина обходу — до самого дна (усі підкаталоги). Можна обмежити глибину: Files.walk(path, maxDepth)

4. Практичні завдання

Завдання 1: Знайти всі зображення в каталозі

Потрібно знайти всі файли з розширенням .jpg, .png, .gif у папці images та вивести їхні імена.

import java.nio.file.*;
import java.util.Set;

Set<String> extensions = Set.of(".jpg", ".png", ".gif");

try (var paths = Files.walk(Path.of("images"))) {
    paths.filter(Files::isRegularFile)
         .filter(path -> {
             String name = path.getFileName().toString().toLowerCase();
             return extensions.stream().anyMatch(name::endsWith);
         })
         .forEach(System.out::println);
}

Завдання 2: Скопіювати всі .txt-файли в іншу папку

import java.nio.file.*;

Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");

try (var paths = Files.walk(sourceDir)) {
    paths.filter(Files::isRegularFile)
         .filter(path -> path.toString().endsWith(".txt"))
         .forEach(path -> {
             try {
                 Path relative = sourceDir.relativize(path);
                 Path target = destDir.resolve(relative);
                 Files.createDirectories(target.getParent());
                 Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
                 System.out.println("Скопійовано: " + path + " -> " + target);
             } catch (Exception e) {
                 System.err.println("Помилка копіювання: " + path);
             }
         });
}

Тут ми зберігаємо структуру підкаталогів.

5. Корисні нюанси

Переваги NIO2

Кросплатформеність

Path сам розбирається з розділювачами папок. Ваш код працюватиме однаково на Windows, Linux, macOS.

Потокова обробка

Методи на кшталт Files.walk повертають потік (Stream<Path>), який можна фільтрувати, перетворювати, збирати в колекції — усе, що вміє Stream API.

Робота з великими директоріями

Старий API міг «впасти», якщо файлів надто багато (наприклад, 100 000 фото). NIO2 обробляє такі випадки легко, адже не завантажує все в пам’ять одразу.

Підтримка симлінків, атрибутів, прав доступу

Можна дізнатися, чи є шлях символьним посиланням (Files.isSymbolicLink(path)), отримати права доступу (Files.getPosixFilePermissions(path)), дізнатися власника файлу та багато іншого.

Порівняння старого і нового API

Операція Старий API (File) Новий API (Path, Files)
Перевірити існування
file.exists()
Files.exists(path)
Отримати розмір
file.length()
Files.size(path)
Список файлів у папці
file.listFiles()
Files.list(path)
Рекурсивний обхід Рекурсія вручну
Files.walk(path)
Копіювання файлу file.renameTo() (криво)
Files.copy(src, dest)
Отримати розширення Розбирати рядок
path.getFileName().toString()
Отримати батька
file.getParentFile()
path.getParent()
Робота з правами Майже ніяк
Files.getPosixFilePermissions(path)

Важливі особливості

Перевірка типу файлу

  • Files.isRegularFile(path) — звичайний файл
  • Files.isDirectory(path) — папка
  • Files.isSymbolicLink(path) — симлінк

Робота з великими директоріями

  • Не варто збирати всі шляхи в список: працюйте з потоками (Stream<Path>) і обробляйте їх у міру надходження.
  • Після завершення роботи потік обов’язково закривається (try-with-resources).

Винятки

  • Майже всі методи можуть викидати IOException — не забувайте обробляти помилки (або прокидати вище).

Обмеження глибини обходу

try (var paths = Files.walk(Path.of("mydir"), 2)) { // лише 2 рівні
    // ...
}

6. Типові помилки під час роботи з NIO2

Помилка № 1: забули закрити потік walk. Якщо не використовувати try-with-resources, можна отримати витік ресурсів — потік файлової системи залишиться відкритим. Завжди використовуйте конструкцію try (var paths = Files.walk(...)) { ... }.

Помилка № 2: не перевірили, що шлях — це директорія. Якщо передати в Files.walk шлях до файлу, а не до папки, можна отримати несподівану поведінку або помилку.

Помилка № 3: не обробили винятки. Практично всі методи NIO2 можуть викинути IOException. Не залишайте ці помилки без уваги — бодай виведіть повідомлення користувачеві або запишіть у журнал.

Помилка № 4: плутанина з розділювачами шляхів. Якщо ви вручну склеюєте шляхи через / або \, ви робите це даремно! Використовуйте Path.of(...) або resolve(...) — вони самі все зроблять як слід.

Помилка № 5: спроба прочитати величезну директорію «в пам’ять». Не збирайте всі шляхи в список, якщо файлів дуже багато — працюйте з потоками (Stream<Path>) і обробляйте їх у міру надходження.

Помилка № 6: забули про кросплатформеність. Не хардкодьте абсолютні шляхи у стилі Windows або Unix. Використовуйте Path і операції на кшталт resolve/relativize — вони зроблять усе правильно на будь-якій ОС.

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