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.renameTo() (криво) | |
| Отримати розширення | Розбирати рядок | |
| Отримати батька | |
|
| Робота з правами | Майже ніяк | |
Важливі особливості
Перевірка типу файлу
- 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 — вони зроблять усе правильно на будь-якій ОС.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ