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 — они сделают правильно на любой ОС.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ