Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 2. Современный NIO.2 (java.nio.file)
Создание файлов и директорий
Класс
Файловая система — это способ организации, хранения, именования, доступа и управления файлами на устройстве хранения данных. Она определяет структуру директорий, права доступа, метаданные файлов и механизмы их поиска.
Создание пустого файла: Files.createFile(Path)
Метод
Параметр
Атомарность создания означает, что файл либо создаётся полностью с указанными атрибутами, либо не создаётся вообще. Другие процессы не увидят файл без атрибутов. Это критично для безопасности: например, если вы создаёте файл с секретными данными и хотите, чтобы он сразу имел ограниченные права
Важное отличие от
Создание одной директории: Files.createDirectory(Path)
Метод
Этот метод полезен, когда вы хотите явно контролировать каждый уровень иерархии и получать ошибку, если структура не соответствует ожиданиям. Например, в установщике приложения, где отсутствие ожидаемой родительской директории может сигнализировать о неправильной конфигурации системы.
Создание вложенных директорий: Files.createDirectories(Path)
Метод
Идемпотентность — это свойство операции, при котором повторное выполнение даёт тот же результат, что и первое. В контексте файловых операций это означает, что
#Java #для_новичков #beginner #IO #NIO #Files #copy
Глава 2. Современный NIO.2 (java.nio.file)
Создание файлов и директорий
Класс
Files предоставляет набор статических методов для создания файловых объектов на диске. Эти методы отличаются от конструкторов старого java.io.File тем, что работают непосредственно с файловой системой и выбрасывают конкретные исключения при ошибках. В отличие от File.createNewFile(), который возвращал boolean без объяснения причин отказа, методы NIO.2 сообщают точно, что пошло не так: файл уже существует, нет прав на запись, родительская директория отсутствует и т.д.Файловая система — это способ организации, хранения, именования, доступа и управления файлами на устройстве хранения данных. Она определяет структуру директорий, права доступа, метаданные файлов и механизмы их поиска.
Создание пустого файла: Files.createFile(Path)
Метод
Files.createFile(Path path, FileAttribute<?>... attrs) создаёт новый пустой файл по указанному пути. Если файл уже существует — выбрасывает FileAlreadyExistsException. Если родительская директория не существует — выбрасывает NoSuchFileException.import java.nio.file.*;
import java.io.IOException;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
public class FileCreator {
public void createEmptyFile(Path path) throws IOException {
// Проверяем, существует ли родительская директория
Path parent = path.getParent();
if (parent != null && Files.notExists(parent)) {
throw new NoSuchFileException(
"Родительская директория не существует: " + parent
);
}
// Создаём файл с правами доступа (только Unix)
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-r--r--");
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
Files.createFile(path, attr);
}
}
Параметр
FileAttribute<?>... attrs — это varargs массив атрибутов файла, которые устанавливаются атомарно при создании. На POSIX-системах через него передаются права доступа. На Windows этот параметр игнорируется, так как модель прав Windows основана на ACL (Access Control List, список контроля доступа), а не на POSIX-битах.Атомарность создания означает, что файл либо создаётся полностью с указанными атрибутами, либо не создаётся вообще. Другие процессы не увидят файл без атрибутов. Это критично для безопасности: например, если вы создаёте файл с секретными данными и хотите, чтобы он сразу имел ограниченные права
rw------- (только владелец).Важное отличие от
File.createNewFile(): Files.createFile() гарантирует, что файл создан именно вашим вызовом. Если между проверкой exists() и вызовом createFile() другой процесс создал файл, createFile() выбросит исключение, а не молча вернёт false. Это устраняет race condition (состояние гонки) — ситуацию, когда результат операции зависит от порядка выполнения конкурирующих потоков или процессов.Создание одной директории: Files.createDirectory(Path)
Метод
Files.createDirectory(Path dir, FileAttribute<?>... attrs) создаёт одну директорию. Все промежуточные директории в пути уже должны существовать. Если директория существует — FileAlreadyExistsException. Если родитель отсутствует — NoSuchFileException.public void createSingleDirectory(Path dir) throws IOException {
// Создаём только конечную директорию
// Предполагаем, что /var/log уже существует
Path appLogs = Paths.get("/var/log/myapp");
Files.createDirectory(appLogs);
}Этот метод полезен, когда вы хотите явно контролировать каждый уровень иерархии и получать ошибку, если структура не соответствует ожиданиям. Например, в установщике приложения, где отсутствие ожидаемой родительской директории может сигнализировать о неправильной конфигурации системы.
Создание вложенных директорий: Files.createDirectories(Path)
Метод
Files.createDirectories(Path dir, FileAttribute<?>... attrs) создаёт директорию и все несуществующие родительские директории в пути. Это аналог команды mkdir -p в Unix. Если конечная директория уже существует — метод молча возвращает управление, не выбрасывая исключение. Если путь существует, но не является директорией — выбрасывает FileAlreadyExistsException.public void createNestedStructure() throws IOException {
Path deepPath = Paths.get("data", "books", "2024", "reviews");
// Создаёт data/, data/books/, data/books/2024/, data/books/2024/reviews/
Files.createDirectories(deepPath);
// Проверяем, что создано
System.out.println("Создана структура: " + deepPath.toAbsolutePath());
System.out.println("Количество сегментов: " + deepPath.getNameCount());
}createDirectories() — идемпотентный метод: повторный вызов с тем же путём не приводит к ошибке. Это делает его идеальным для инициализации структуры приложения при старте.Идемпотентность — это свойство операции, при котором повторное выполнение даёт тот же результат, что и первое. В контексте файловых операций это означает, что
createDirectories() можно вызывать сколько угодно раз — структура останется корректной.#Java #для_новичков #beginner #IO #NIO #Files #copy
👍6
Создание временных файлов и директорий
Files.createTempFile(String prefix, String suffix)
Создаёт временный файл в системной временной директории (
Префикс и суффикс позволяют идентифицировать назначение файла. Атомарность генерации имени означает, что даже при конкурентном вызове из нескольких потоков имена не пересекутся.
Files.createTempFile(Path dir, String prefix, String suffix)
Создаёт временный файл в указанной директории, а не в системной временной.
Это важно, когда системная временная директория (
tmpfs — это виртуальная файловая система, хранящая данные в оперативной памяти (RAM) вместо диска. Она быстрая, но ограничена объёмом ОЗУ.
Files.createTempDirectory(String prefix)
Создаёт временную директорию в системной временной директории.
Files.createTempDirectory(Path dir, String prefix)
Создаёт временную директорию в указанной родительской директории.
Копирование файлов: Files.copy()
Базовое копирование: Files.copy(Path source, Path target, CopyOption... options)
Метод копирует файл или директорию по указанному пути. Для директории копируется только сама директория как пустая сущность, без содержимого. Для рекурсивного копирования дерева требуется ручной обход.
StandardCopyOption.REPLACE_EXISTING
Разрешает перезапись существующего файла назначения. Без этой опции
StandardCopyOption.COPY_ATTRIBUTES
Копирует метаданные файла: временные метки (creation time, last modified time, last access time) и POSIX-права (на Unix). Без этой опции новый файл получает текущее время и права по умолчанию (umask).
umask (user file-creation mode mask) — это маска прав доступа в Unix, которая определяет, какие биты прав сбрасываются при создании нового файла. Например, umask
StandardCopyOption.ATOMIC_MOVE
Не применяется к
Копирование из потока: Files.copy(InputStream in, Path target, CopyOption... options)
Читает все байты из
Метод возвращает количество скопированных байт как
Копирование в поток: Files.copy(Path source, OutputStream out)
Читает все байты из файла и записывает их в
Этот метод эффективен, так как внутри использует нативный механизм zero-copy (нулевое копирование) через
Копирование директорий: Files.copy() не рекурсивно
Критически важно понимать:
В этом коде
Pre-order обход — это стратегия обхода дерева, при которой узел обрабатывается до его потомков. В контексте файловой системы это означает, что директория создаётся перед файлами внутри неё.
#Java #для_новичков #beginner #IO #NIO #Files #copy
Files.createTempFile(String prefix, String suffix)
Создаёт временный файл в системной временной директории (
java.io.tmpdir). Имя генерируется атомарно и гарантированно уникально в пределах JVM-процесса.Path tempFile = Files.createTempFile("report_", ".tmp");
// Результат: /tmp/report_1234567890.tmpПрефикс и суффикс позволяют идентифицировать назначение файла. Атомарность генерации имени означает, что даже при конкурентном вызове из нескольких потоков имена не пересекутся.
Files.createTempFile(Path dir, String prefix, String suffix)
Создаёт временный файл в указанной директории, а не в системной временной.
Path customDir = Paths.get("/app/temp");
Files.createDirectories(customDir);
Path tempInCustom = Files.createTempFile(customDir, "session_", ".dat");Это важно, когда системная временная директория (
/tmp на Linux) монтируется в tmpfs (файловая система в памяти) с ограниченным размером, а ваши временные файлы большие. В таком случае лучше использовать директорию на диске.tmpfs — это виртуальная файловая система, хранящая данные в оперативной памяти (RAM) вместо диска. Она быстрая, но ограничена объёмом ОЗУ.
/tmp на многих Linux-дистрибутивах монтируется как tmpfs.Files.createTempDirectory(String prefix)
Создаёт временную директорию в системной временной директории.
Path tempDir = Files.createTempDirectory("batch_processing_");
// Результат: /tmp/batch_processing_1234567890/Files.createTempDirectory(Path dir, String prefix)
Создаёт временную директорию в указанной родительской директории.
Path workDir = Paths.get("/var/app/work");
Path tempWorkDir = Files.createTempDirectory(workDir, "job_");Копирование файлов: Files.copy()
Базовое копирование: Files.copy(Path source, Path target, CopyOption... options)
Метод копирует файл или директорию по указанному пути. Для директории копируется только сама директория как пустая сущность, без содержимого. Для рекурсивного копирования дерева требуется ручной обход.
Path src = Paths.get("/data/report.txt");
Path dst = Paths.get("/backup/report.txt");
Files.copy(src, dst); // FileAlreadyExistsException, если dst существуетStandardCopyOption.REPLACE_EXISTING
Разрешает перезапись существующего файла назначения. Без этой опции
Files.copy() выбросит FileAlreadyExistsException.Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
StandardCopyOption.COPY_ATTRIBUTES
Копирует метаданные файла: временные метки (creation time, last modified time, last access time) и POSIX-права (на Unix). Без этой опции новый файл получает текущее время и права по умолчанию (umask).
umask (user file-creation mode mask) — это маска прав доступа в Unix, которая определяет, какие биты прав сбрасываются при создании нового файла. Например, umask
022 означает, что у новых файлов не будут прав на запись для группы и остальных.Files.copy(src, dst,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES
);
StandardCopyOption.ATOMIC_MOVE
Не применяется к
copy(), только к move(). При передаче в copy() выбросит UnsupportedOperationException.Копирование из потока: Files.copy(InputStream in, Path target, CopyOption... options)
Читает все байты из
InputStream и записывает их в файл. Поток закрывается автоматически. Удобно для загрузки данных из сети или других источников.import java.net.URL;
public void downloadFile(String urlString, Path target) throws IOException {
URL url = new URL(urlString);
try (InputStream in = url.openStream()) {
// Копируем всё содержимое потока в файл
// CREATE — создать файл, если нет
// REPLACE_EXISTING — перезаписать, если есть
long bytesCopied = Files.copy(in, target,
StandardCopyOption.REPLACE_EXISTING
);
System.out.println("Скопировано байт: " + bytesCopied);
}
// InputStream закрыт автоматически try-with-resources
}
Метод возвращает количество скопированных байт как
long. Это полезно для логирования и проверки целостности.Копирование в поток: Files.copy(Path source, OutputStream out)
Читает все байты из файла и записывает их в
OutputStream. Поток не закрывается автоматически — управление потоком остаётся на вызывающей стороне.public void streamFileToResponse(Path file, OutputStream responseOut) throws IOException {
// Отправляем файл в HTTP-ответ
// responseOut управляется внешним кодом (сервлет, фреймворк)
long bytesSent = Files.copy(file, responseOut);
responseOut.flush(); // явно сбрасываем буфер
}Этот метод эффективен, так как внутри использует нативный механизм zero-copy (нулевое копирование) через
transferTo() файлового канала, если поток поддерживает запись в канал. Zero-copy означает, что данные передаются от диска к сетевому сокету без промежуточного копирования в пользовательское пространство JVM, что снижает нагрузку на CPU и память.Копирование директорий: Files.copy() не рекурсивно
Критически важно понимать:
Files.copy() для директории копирует только саму директорию как пустую сущность. Все файлы и поддиректории внутри не копируются. Для полного копирования дерева требуется ручной обход.import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
public class DirectoryCopier {
public void copyDirectory(Path source, Path target) throws IOException {
// Проверяем, что источник — директория
if (!Files.isDirectory(source)) {
throw new NotDirectoryException(source.toString());
}
// Создаём корневую директорию назначения
// createDirectories идемпотентен — не бросает исключение, если уже есть
Files.createDirectories(target);
// Обходим дерево источника
try (Stream<Path> walk = Files.walk(source)) {
walk.forEach(srcPath -> {
try {
// Вычисляем относительный путь от корня источника
Path relative = source.relativize(srcPath);
// Резолвим его относительно корня назначения
Path dstPath = target.resolve(relative);
if (Files.isDirectory(srcPath)) {
// Создаём директорию
Files.createDirectories(dstPath);
} else {
// Копируем файл
Files.copy(srcPath, dstPath,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES
);
}
} catch (IOException e) {
// Оборачиваем в unchecked для использования внутри forEach
throw new UncheckedIOException(e);
}
});
}
}
}
В этом коде
Files.walk(source) возвращает поток всех путей в дереве источника, включая корень. source.relativize(srcPath) вычисляет относительный путь от корня, а target.resolve(relative) строит соответствующий путь в дереве назначения. Порядок обхода гарантирован — walk() обходит в порядке pre-order (сначала родитель, потом дети), поэтому директории создаются до файлов внутри них.Pre-order обход — это стратегия обхода дерева, при которой узел обрабатывается до его потомков. В контексте файловой системы это означает, что директория создаётся перед файлами внутри неё.
#Java #для_новичков #beginner #IO #NIO #Files #copy
👍6
Путь байтов в памяти JVM при создании и копировании
Создание файла: путь данных
Когда вы вызываете
Объект
Метод
Байты пути копируются из кучи JVM в нативную память через JNI. Системный вызов
Вновь созданный файл представлен в ядре ОС структурой inode (Unix) или записью в MFT (NTFS). Права доступа устанавливаются через
Возвращаемое значение
Копирование файла: путь данных
При
Источник и назначение — объекты
Если источник и назначение на одном
Если оптимизация недоступна, выполняется классическое копирование: данные читаются из источника через кэш страниц ОС в нативную память, затем записываются в назначение. При этом данные проходят через пространство пользователя (user space) JVM — два системных вызова на каждый блок:
Для больших файлов внутренняя реализация
Если передан
Все временные объекты — буфер копирования, массивы байтов пути — создаются в Young Generation. Если операция быстрая (маленький файл), они уничтожаются при следующей Minor GC. Если файл большой и операция длительная, буфер может пережить несколько циклов Minor GC и быть перемещён в Survivor Space, а затем в Old Generation при достижении порога возраста.
Survivor Space — это одно из двух пространств (S0 или S1) в Young Generation, куда перемещаются объекты, пережившие Minor GC. Объекты "прыгают" между S0 и S1, и при достижении определённого количества циклов сборки (tenuring threshold) перемещаются в Old Generation.
Создание директорий: путь данных
Строится промежуточный
Выполняется системный вызов
Если переданы
Промежуточные объекты
#Java #для_новичков #beginner #IO #NIO #Files #copy
Создание файла: путь данных
Когда вы вызываете
Files.createFile(path):Объект
Path уже находится в куче (Heap). Строковые сегменты пути — это String-объекты, хранящие массивы символов (char[]) в куче.Метод
createFile() вызывает FileSystemProvider.createFile(). Провайдер извлекает байтовое представление пути: на Unix это массив байтов в кодировке UTF-8, на Windows — UTF-16LE (wchar_t[]).Байты пути копируются из кучи JVM в нативную память через JNI. Системный вызов
open() с флагом O_CREAT | O_EXCL (POSIX) атомарно создаёт файл, если он не существует. Флаг O_EXCL гарантирует, что если файл уже есть, вызов вернёт ошибку EEXIST — JVM преобразует её в FileAlreadyExistsException.Вновь созданный файл представлен в ядре ОС структурой inode (Unix) или записью в MFT (NTFS). Права доступа устанавливаются через
fchmod() или аналогичный вызов, если были переданы FileAttribute.Возвращаемое значение
createFile() — это объект Path, переданный в метод (тот же самый объект, без копирования). Никаких новых объектов в куче не создаётся, за исключением возможного внутреннего объекта исключения, если операция не удалась.Копирование файла: путь данных
При
Files.copy(source, target):Источник и назначение — объекты
Path в куче. Метод определяет FileStore (хранилище) для обоих путей через FileSystemProvider.getFileStore().Если источник и назначение на одном
FileStore (один раздел диска), некоторые провайдеры могут использовать оптимизацию reflink (copy-on-write копирование) — вместо физического копирования блоков данных создаётся новая ссылка на те же блоки. Изменение любой из копий приведёт к физическому копированию изменённых блоков. Это поддерживается файловыми системами Btrfs и XFS на Linux.Если оптимизация недоступна, выполняется классическое копирование: данные читаются из источника через кэш страниц ОС в нативную память, затем записываются в назначение. При этом данные проходят через пространство пользователя (user space) JVM — два системных вызова на каждый блок:
read() и write().Для больших файлов внутренняя реализация
Files.copy() использует буфер фиксированного размера (обычно 8 КБ или 32 КБ). Этот буфер — массив byte[] в куче, который создаётся один раз и переиспользуется. При копировании файла размером 1 ГБ буфер создаётся один раз, а не для каждого блока, что минимизирует давление на GC.Если передан
COPY_ATTRIBUTES, после копирования данных выполняется дополнительный системный вызов для установки метаданных: utimensat() для временных меток, fchmod() для прав.Все временные объекты — буфер копирования, массивы байтов пути — создаются в Young Generation. Если операция быстрая (маленький файл), они уничтожаются при следующей Minor GC. Если файл большой и операция длительная, буфер может пережить несколько циклов Minor GC и быть перемещён в Survivor Space, а затем в Old Generation при достижении порога возраста.
Survivor Space — это одно из двух пространств (S0 или S1) в Young Generation, куда перемещаются объекты, пережившие Minor GC. Объекты "прыгают" между S0 и S1, и при достижении определённого количества циклов сборки (tenuring threshold) перемещаются в Old Generation.
Создание директорий: путь данных
Files.createDirectories() выполняет итерацию по сегментам пути. Для каждого сегмента:Строится промежуточный
Path (в куче) — это лёгкий объект, создаваемый быстро.Выполняется системный вызов
mkdir(). Если директория уже существует — ошибка EEXIST, которую метод игнорирует (идемпотентность).Если переданы
FileAttribute, применяются chmod().Промежуточные объекты
Path становятся мусором сразу после итерации. При создании глубокой иерархии (data/books/2024/reviews) создаётся 4 временных объекта Path — все они собираются при первой Minor GC.#Java #для_новичков #beginner #IO #NIO #Files #copy
👍6
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 2. Современный NIO.2 (java.nio.file)
Перемещение и переименование файлов и директорий
Перемещение и удаление — это операции, которые завершают жизненный цикл файла в файловой системе.
В отличие от копирования, которое создаёт новую копию данных, перемещение часто не требует физического перемещения байтов на диске — достаточно изменить метаданные в файловой системе.
Удаление, в свою очередь, освобождает inode и блоки данных, но реальное освобождение дискового пространства зависит от того, есть ли другие ссылки на те же данные. Класс
inode (index node) — это структура данных в файловых системах Unix, хранящая метаданные файла: права доступа, владелец, размер, временные метки и указатели на блоки данных. Имя файла — это просто ссылка на inode в директории.
Files.move(Path source, Path target) — перемещение и переименование
Метод
Разница между копированием и перемещением
Копирование (
Перемещение в пределах одного
Перемещение между разными
FileStore — абстракция над хранилищем файловой системы (раздел диска, том, сетевой ресурс). Каждый раздел имеет свой
Опции перемещения
StandardCopyOption.REPLACE_EXISTING
Разрешает перезапись существующего файла или директории в точке назначения. Без этой опции
Важно: при перезаписи директории целевая директория должна быть пустой. Если
StandardCopyOption.ATOMIC_MOVE
Выполняет атомарное перемещение. Это означает, что операция либо завершится целиком, либо не произойдёт вообще. Другие процессы никогда не увидят промежуточного состояния, где ни источник, ни назначение не существуют.
Ограничения
Источник и назначение должны находиться на одном
Нельзя комбинировать с
Если назначение существует и передан
Атомарность в контексте файловых систем означает неделимость операции. Для
Пример: перемещение обработанного файла в архивную папку
В этом примере
#Java #для_новичков #beginner #IO #NIO #Files #move #delete
Глава 2. Современный NIO.2 (java.nio.file)
Перемещение и переименование файлов и директорий
Перемещение и удаление — это операции, которые завершают жизненный цикл файла в файловой системе.
В отличие от копирования, которое создаёт новую копию данных, перемещение часто не требует физического перемещения байтов на диске — достаточно изменить метаданные в файловой системе.
Удаление, в свою очередь, освобождает inode и блоки данных, но реальное освобождение дискового пространства зависит от того, есть ли другие ссылки на те же данные. Класс
Files предоставляет атомарные и безопасные методы для этих операций, каждый из которых имеет чёткую семантику ошибок.inode (index node) — это структура данных в файловых системах Unix, хранящая метаданные файла: права доступа, владелец, размер, временные метки и указатели на блоки данных. Имя файла — это просто ссылка на inode в директории.
Files.move(Path source, Path target) — перемещение и переименование
Метод
Files.move(Path source, Path target, CopyOption... options) выполняет перемещение файла или директории из source в target. На уровне файловой системы это может быть либо переименование в пределах одного раздела (атомарная операция, изменяющая только записи в директориях), либо копирование с последующим удалением источника (если источник и назначение на разных разделах или файловых системах).import java.nio.file.*;
import java.io.IOException;
public class FileMover {
public void renameFile(Path oldName, Path newName) throws IOException {
// Простое переименование в той же директории
Files.move(oldName, newName);
}
public void moveToArchive(Path file, Path archiveDir) throws IOException {
// Перемещение в другую директорию
Path target = archiveDir.resolve(file.getFileName());
Files.move(file, target);
}
}
Разница между копированием и перемещением
Копирование (
Files.copy) создаёт независимую копию данных. После копирования источник и назначение — это два разных файла с разными inode, каждый занимает своё дисковое пространство. Изменение одного не влияет на другой.Перемещение в пределах одного
FileStore (одного раздела диска) — это операция над метаданными. Файловая система изменяет запись в родительской директории источника (удаляет ссылку на inode) и добавляет запись в родительскую директорию назначения. Сами данные на диске не перемещаются. inode остаётся тем же, временные метки данных не меняются. Это выполняется за один системный вызов rename() и является атомарным.Перемещение между разными
FileStore (разные разделы, разные диски, сетевые ФС) невозможно как атомарная операция над метаданными. JVM выполняет копирование данных из источника в назначение, а затем удаляет источник. Это неатомарно: если копирование прошло успешно, но удаление источника не удалось (например, файл заблокирован), вы получите частичный результат — данные продублированы, а исходный файл остался на месте.FileStore — абстракция над хранилищем файловой системы (раздел диска, том, сетевой ресурс). Каждый раздел имеет свой
FileStore, и перемещение между разными FileStore требует физического копирования данных.Опции перемещения
StandardCopyOption.REPLACE_EXISTING
Разрешает перезапись существующего файла или директории в точке назначения. Без этой опции
Files.move() выбросит FileAlreadyExistsException, если target уже существует.Path src = Paths.get("/data/report_v1.txt");
Path dst = Paths.get("/data/report.txt");
// Перезаписываем старый отчёт новым
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);Важно: при перезаписи директории целевая директория должна быть пустой. Если
target — непустая директория, даже с REPLACE_EXISTING будет выброшено DirectoryNotEmptyException.StandardCopyOption.ATOMIC_MOVE
Выполняет атомарное перемещение. Это означает, что операция либо завершится целиком, либо не произойдёт вообще. Другие процессы никогда не увидят промежуточного состояния, где ни источник, ни назначение не существуют.
Path src = Paths.get("/data/temp_report.txt");
Path dst = Paths.get("/data/report.txt");
// Атомарное переименование: читатели либо видят старый файл, либо новый
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);Ограничения
ATOMIC_MOVE:Источник и назначение должны находиться на одном
FileStore. Если это не так, выбрасывается AtomicMoveNotSupportedException.Нельзя комбинировать с
COPY_ATTRIBUTES — атомарное перемещение не копирует атрибуты, оно переносит inode целиком, вместе со всеми его метаданными.Если назначение существует и передан
REPLACE_EXISTING, перезапись тоже будет атомарной.Атомарность в контексте файловых систем означает неделимость операции. Для
rename() атомарность гарантируется на уровне ядра ОС: запись в структуры директорий защищена блокировками, и другие процессы видят либо старое имя, либо новое, но не промежуточное состояние.Пример: перемещение обработанного файла в архивную папку
import java.nio.file.*;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class ArchiveMover {
private static final DateTimeFormatter DATE_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault());
public void archiveProcessedFile(Path processedFile, Path archiveRoot) throws IOException {
// Проверяем, что источник существует и является файлом
if (!Files.isRegularFile(processedFile)) {
throw new NoSuchFileException("Файл не найден или не является обычным файлом: " + processedFile);
}
// Создаём архивную директорию с текущей датой: /archive/2024-07-14/
String today = DATE_FORMAT.format(Instant.now());
Path datedArchive = archiveRoot.resolve(today);
Files.createDirectories(datedArchive);
// Формируем целевой путь
String fileName = processedFile.getFileName().toString();
Path target = datedArchive.resolve(fileName);
// Если файл с таким именем уже есть в архиве — добавляем счётчик
Path finalTarget = resolveUniqueName(target);
// Атомарное перемещение
Files.move(processedFile, finalTarget, StandardCopyOption.ATOMIC_MOVE);
System.out.println("Файл перемещён в архив: " + finalTarget.toAbsolutePath());
}
private Path resolveUniqueName(Path target) {
if (Files.notExists(target)) {
return target;
}
// Генерируем уникальное имя: file.txt -> file_1.txt, file_2.txt
String fileName = target.getFileName().toString();
int lastDot = fileName.lastIndexOf('.');
String base = lastDot > 0 ? fileName.substring(0, lastDot) : fileName;
String ext = lastDot > 0 ? fileName.substring(lastDot) : "";
Path parent = target.getParent();
int counter = 1;
Path candidate;
do {
candidate = parent.resolve(base + "_" + counter + ext);
counter++;
} while (Files.exists(candidate));
return candidate;
}
public static void main(String[] args) {
try {
Path file = Paths.get("/app/inbox/order_12345.xml");
Path archive = Paths.get("/app/archive");
new ArchiveMover().archiveProcessedFile(file, archive);
} catch (AtomicMoveNotSupportedException e) {
System.err.println("Атомарное перемещение невозможно — источник и архив на разных разделах");
// Fallback: копирование + удаление
} catch (IOException e) {
System.err.println("Ошибка архивирования: " + e.getMessage());
}
}
}
В этом примере
ATOMIC_MOVE гарантирует, что в процессе перемещения читатели директории inbox либо видят файл на месте, либо не видят его вообще (уже в архиве). Промежуточного состояния "файл исчез" не возникает. Если архив находится на другом разделе, ловим AtomicMoveNotSupportedException и применяем fallback-стратегию.#Java #для_новичков #beginner #IO #NIO #Files #move #delete
👍5
Удаление файлов и директорий
Files.delete(Path) — строгое удаление
Метод
Строгость этого метода полезна, когда вы ожидаете, что файл существует, и хотите знать, если это не так. Например, при очистке временного файла, который обязательно должен был создаться предыдущим шагом.
Files.deleteIfExists(Path) — условное удаление
Метод
Этот метод идеален для идемпотентной очистки: можно вызывать многократно без ошибок.
DirectoryNotEmptyException
Оба метода
Обход дерева с удалением всех файлов
Для рекурсивного удаления директории со всем содержимым используется обход дерева в обратном порядке: сначала удаляются файлы и пустые поддиректории на самом глубоком уровне, затем — родительские директории.
Почему
UncheckedIOException — это обёртка вокруг
Пример: удаление всех .tmp файлов во временной папке
В этом примере
DirectoryStream — это интерфейс в NIO.2, представляющий поток записей директории. В отличие от
#Java #для_новичков #beginner #IO #NIO #Files #move #delete
Files.delete(Path) — строгое удаление
Метод
Files.delete(Path path) удаляет файл, директорию или символическую ссылку. Если объект не существует — выбрасывает NoSuchFileException. Если путь — непустая директория — выбрасывает DirectoryNotEmptyException. Если нет прав на удаление — AccessDeniedException.Path file = Paths.get("/tmp/old_data.tmp");
Files.delete(file); // NoSuchFileException, если файла нетСтрогость этого метода полезна, когда вы ожидаете, что файл существует, и хотите знать, если это не так. Например, при очистке временного файла, который обязательно должен был создаться предыдущим шагом.
Files.deleteIfExists(Path) — условное удаление
Метод
Files.deleteIfExists(Path path) удаляет файл, если он существует. Возвращает true, если файл был удалён, false — если файла не было. Не выбрасывает исключение при отсутствии файла, но всё ещё выбрасывает DirectoryNotEmptyException и AccessDeniedException.Path tempFile = Paths.get("/tmp/session_abc123.tmp");
boolean wasDeleted = Files.deleteIfExists(tempFile);
if (wasDeleted) {
System.out.println("Временный файл удалён");
} else {
System.out.println("Файл уже был удалён или не существовал");
}Этот метод идеален для идемпотентной очистки: можно вызывать многократно без ошибок.
DirectoryNotEmptyException
Оба метода
delete() и deleteIfExists() не удаляют непустые директории. Это ограничение на уровне файловой системы: удаление директории требует сначала удалить все её содержимое. Файловая система хранит список записей в директории, и ядро ОС отказывает в удалении, пока список не пуст.Path dir = Paths.get("/tmp/myapp");
try {
Files.delete(dir); // DirectoryNotEmptyException, если внутри есть файлы
} catch (DirectoryNotEmptyException e) {
// Нужно сначала удалить содержимое
}Обход дерева с удалением всех файлов
Для рекурсивного удаления директории со всем содержимым используется обход дерева в обратном порядке: сначала удаляются файлы и пустые поддиректории на самом глубоком уровне, затем — родительские директории.
import java.nio.file.*;
import java.io.IOException;
import java.util.Comparator;
import java.util.stream.Stream;
public class TreeDeleter {
public void deleteDirectoryRecursively(Path root) throws IOException {
if (!Files.exists(root)) {
return; // Нечего удалять
}
// walk возвращает Stream<Path> в порядке pre-order (родитель до детей)
// sorted(reverseOrder()) переворачивает: сначала глубокие пути, потом корень
try (Stream<Path> walk = Files.walk(root)) {
walk.sorted(Comparator.reverseOrder())
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
// Оборачиваем в UncheckedIOException для использования в forEach
throw new UncheckedIOException(
"Не удалось удалить: " + path, e
);
}
});
} catch (UncheckedIOException e) {
// Распаковываем обратно в checked исключение
throw e.getCause();
}
}
}
Почему
Comparator.reverseOrder()? Метод Files.walk() обходит дерево в порядке pre-order: сначала корень, потом его дети. Если мы будем удалять в этом порядке, попытка удалить корневую директорию первой выбросит DirectoryNotEmptyException, так как дети ещё не удалены. reverseOrder() сортирует пути по строковому представлению в обратном порядке, что гарантирует, что более глубокие пути (с большим количеством сегментов) обрабатываются раньше.UncheckedIOException — это обёртка вокруг
IOException, наследующая RuntimeException. Она нужна потому, что функциональные интерфейсы в Java (например, Consumer в forEach) не могут выбрасывать checked-исключения. Оборачивая IOException в UncheckedIOException, мы пробрасываем его через лямбду, а затем распаковываем вне.Пример: удаление всех .tmp файлов во временной папке
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
public class TempCleaner {
private static final Path TEMP_DIR = Paths.get(System.getProperty("java.io.tmpdir"));
public void cleanTempFiles() throws IOException {
// Проверяем, что TEMP_DIR существует и является директорией
if (!Files.isDirectory(TEMP_DIR)) {
throw new NotDirectoryException(TEMP_DIR.toString());
}
// Обходим только верхний уровень временной директории
// newDirectoryStream — более лёгкая альтернатива walk для одного уровня
try (DirectoryStream<Path> stream = Files.newDirectoryStream(TEMP_DIR)) {
for (Path entry : stream) {
if (Files.isRegularFile(entry) && entry.toString().endsWith(".tmp")) {
try {
Files.delete(entry);
System.out.println("Удалён: " + entry.getFileName());
} catch (IOException e) {
// Логируем, но продолжаем удаление остальных
System.err.println("Не удалось удалить " + entry.getFileName()
+ ": " + e.getMessage());
}
}
}
}
}
// Альтернатива: удаление .tmp файлов рекурсивно во всём поддереве
public void cleanTempFilesRecursive(Path root) throws IOException {
try (Stream<Path> walk = Files.walk(root)) {
walk.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".tmp"))
.forEach(p -> {
try {
Files.delete(p);
System.out.println("Удалён: " + p);
} catch (IOException e) {
System.err.println("Ошибка удаления " + p + ": " + e.getMessage());
}
});
}
}
// Удаление .tmp файлов старше N дней
public void cleanOldTempFiles(int daysOld) throws IOException {
long cutoff = System.currentTimeMillis() - (daysOld * 24 * 60 * 60 * 1000L);
try (Stream<Path> walk = Files.walk(TEMP_DIR)) {
walk.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".tmp"))
.filter(p -> {
try {
return Files.getLastModifiedTime(p).toMillis() < cutoff;
} catch (IOException e) {
return false; // Не удалось прочитать — пропускаем
}
})
.forEach(p -> {
try {
Files.delete(p);
System.out.println("Удалён старый файл: " + p.getFileName());
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
});
}
}
public static void main(String[] args) {
try {
TempCleaner cleaner = new TempCleaner();
cleaner.cleanTempFiles();
cleaner.cleanOldTempFiles(7); // удалить .tmp старше 7 дней
} catch (IOException e) {
System.err.println("Ошибка очистки: " + e.getMessage());
}
}
}
В этом примере
newDirectoryStream() — более лёгкая альтернатива Files.walk() для обхода только одного уровня. Она не строит дерево и не буферизует пути, а возвращает итератор, который читает директорию порциями. Это экономит память при работе с директориями, содержащими миллионы файлов.DirectoryStream — это интерфейс в NIO.2, представляющий поток записей директории. В отличие от
Stream<Path>, он не поддерживает промежуточные операции (filter, map), но эффективнее для простого обхода, так как не создаёт промежуточных объектов потока.#Java #для_новичков #beginner #IO #NIO #Files #move #delete
👍6
Путь байтов в памяти JVM при перемещении и удалении
Перемещение: путь данных
При
Объекты
Если источник и назначение на одном
Удаляется запись в родительской директории источника.
Создаётся запись в родительской директории назначения, указывающая на тот же inode.
Блоки данных на диске не перемещаются.
Поскольку данные не копируются, в куче JVM не создаётся никаких буферов или массивов байтов. Объекты
Если
Если источник и назначение на разных
При этом:
Создаётся буфер
Данные копируются блоками: чтение в буфер, запись из буфера.
После успешного копирования источник удаляется через
Буфер становится мусором и собирается Minor GC.
Удаление: путь данных
При
Объект
Выполняется системный вызов
Удаляет запись в родительской директории.
Уменьшает счётчик ссылок (reference count) на inode на 1.
Если счётчик ссылок достигает 0 и нет открытых файловых дескрипторов на этот inode, блоки данных помечаются как свободные.
Если счётчик 0, но дескрипторы открыты (например, другой процесс читает файл), блоки остаются занятыми до закрытия всех дескрипторов. Файл становится "невидимым" в файловой системе, но продолжает занимать место на диске.
Никаких новых объектов в куче JVM не создаётся. Операция полностью работает с метаданными файловой системы.
При удалении директории через
Рекурсивное удаление: путь данных и GC
При рекурсивном удалении через
Альтернатива без материализации — обход вручную через
FileVisitor — это интерфейс в NIO.2, определяющий callback-методы для обхода дерева:
#Java #для_новичков #beginner #IO #NIO #Files #move #delete
Перемещение: путь данных
При
Files.move(source, target, ATOMIC_MOVE):Объекты
source и target — это Path в куче. Их строковые представления извлекаются и кодируются в байты пути в нативной кодировке.Если источник и назначение на одном
FileStore, выполняется системный вызов rename() (POSIX) или MoveFileEx() (Windows). Этот вызов работает исключительно с метаданными файловой системы:Удаляется запись в родительской директории источника.
Создаётся запись в родительской директории назначения, указывающая на тот же inode.
Блоки данных на диске не перемещаются.
Поскольку данные не копируются, в куче JVM не создаётся никаких буферов или массивов байтов. Объекты
source и target остаются в куче как обычные объекты Path. Если они локальные переменные метода, они становятся мусором после выхода из метода и собираются при следующей Minor GC.Если
REPLACE_EXISTING передан и целевой файл существует, файловая система сначала удаляет целевой файл (уменьшает счётчик ссылок на его inode), а затем выполняет rename(). Это тоже атомарно на уровне ядра.Если источник и назначение на разных
FileStore, ATOMIC_MOVE выбросит AtomicMoveNotSupportedException до выполнения каких-либо операций. Без ATOMIC_MOVE JVM выполняет fallback: копирование через буфер в куче + удаление источника. При этом:
Создаётся буфер
byte[] в Young Generation (обычно 8–32 КБ).Данные копируются блоками: чтение в буфер, запись из буфера.
После успешного копирования источник удаляется через
unlink().Буфер становится мусором и собирается Minor GC.
Удаление: путь данных
При
Files.delete(path):Объект
Path в куче преобразуется в байты пути в нативной памяти.Выполняется системный вызов
unlink() (POSIX) или DeleteFile() (Windows). Ядро ОС:Удаляет запись в родительской директории.
Уменьшает счётчик ссылок (reference count) на inode на 1.
Если счётчик ссылок достигает 0 и нет открытых файловых дескрипторов на этот inode, блоки данных помечаются как свободные.
Если счётчик 0, но дескрипторы открыты (например, другой процесс читает файл), блоки остаются занятыми до закрытия всех дескрипторов. Файл становится "невидимым" в файловой системе, но продолжает занимать место на диске.
Никаких новых объектов в куче JVM не создаётся. Операция полностью работает с метаданными файловой системы.
При удалении директории через
rmdir() (POSIX) ядро проверяет, что директория содержит только записи . и .. (текущая и родительская). Если есть другие записи — возвращается ENOTEMPTY, который JVM преобразует в DirectoryNotEmptyException.Рекурсивное удаление: путь данных и GC
При рекурсивном удалении через
Files.walk():Files.walk(root) создаёт Stream<Path>, который лениво обходит дерево. Каждый вызов walk() порождает внутренние объекты итератора директории в куче.sorted(Comparator.reverseOrder()) материализует весь поток в список для сортировки. Это означает, что все пути дерева загружаются в память одновременно. Для дерева из миллиона файлов это создаст миллион объектов Path в куче, что может привести к OutOfMemoryError.Альтернатива без материализации — обход вручную через
FileVisitor:public void deleteRecursivelySafe(Path root) throws IOException {
// FileVisitor — интерфейс для обхода дерева с callback-методами
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
if (exc != null) {
throw exc; // ошибка при обходе поддиректории
}
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}FileVisitor — это интерфейс в NIO.2, определяющий callback-методы для обхода дерева:
preVisitDirectory (перед входом в директорию), visitFile (при обнаружении файла), visitFileFailed (при ошибке доступа к файлу), postVisitDirectory (после выхода из директории). SimpleFileVisitor — адаптер с пустыми реализациями по умолчанию.Files.walkFileTree() не материализует всё дерево в память. Он обходит директории рекурсивно, вызывая callback'и по мере продвижения. Это безопасно для огромных деревьев, так как в памяти хранится только стек текущего пути обхода, а не всё дерево целиком. Объекты Path создаются и сразу становятся мусором после выхода из callback'а, собираясь Minor GC.#Java #для_новичков #beginner #IO #NIO #Files #move #delete
👍5
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 2. Современный NIO.2 (java.nio.file)
Атрибуты файлов — BasicFileAttributes
Каждый файл или директория в файловой системе хранит не только данные, но и метаданные — информацию о самом файле. Эти метаданные называются атрибутами файла и включают размер, временные метки (когда файл создан, изменён или к нему обращались), тип файла (обычный файл, директория, символическая ссылка) и права доступа. В Java NIO.2 работа с атрибутами вынесена в отдельную иерархию интерфейсов, корнем которой является
Метаданные — это данные о данных. В контексте файловой системы это не содержимое файла, а его характеристики: размер, время создания, права доступа, владелец и т.д.
Интерфейс
Что такое атрибуты файла
Атрибуты файла делятся на несколько категорий:
Базовые атрибуты (
Размер файла (
Время создания (
Время последней модификации (
Время последнего доступа (
Тип файла — флаги
POSIX-атрибуты (
Владелец (
Группа (
Права доступа (
DOS-атрибуты (
Read-only — файл доступен только для чтения.
Hidden — скрытый файл.
System — системный файл.
Archive — флаг архивации.
Временные метки в NIO.2 представлены классом
Наносекунда — единица времени, равная одной миллиардной доле секунды (10^-9 с). Современные файловые системы (ext4, NTFS, APFS) хранят временные метки с наносекундной точностью, поэтому FileTime поддерживает эту гранулярность, в отличие от Date, который ограничен миллисекундами.
Чтение атрибутов: Files.readAttributes()
Files.readAttributes(Path, Class<A>
Этот метод — основной способ чтения атрибутов файла. Он принимает путь, класс интерфейса атрибутов и опции следования по символическим ссылкам. Возвращает объект, реализующий запрошенный интерфейс.
Метод
Системный вызов stat() — это вызов ядра Unix, который возвращает структуру stat с полной информацией о файле: inode, размер, права, временные метки, количество ссылок. Аналог на Windows — GetFileAttributesEx().
Различия между readAttributes() и отдельными методами Files
При интенсивной работе с файловой системой (например, сканирование дерева из миллиона файлов) использование
#Java #для_новичков #beginner #IO #NIO #Files #BasicFileAttributes
Глава 2. Современный NIO.2 (java.nio.file)
Атрибуты файлов — BasicFileAttributes
Каждый файл или директория в файловой системе хранит не только данные, но и метаданные — информацию о самом файле. Эти метаданные называются атрибутами файла и включают размер, временные метки (когда файл создан, изменён или к нему обращались), тип файла (обычный файл, директория, символическая ссылка) и права доступа. В Java NIO.2 работа с атрибутами вынесена в отдельную иерархию интерфейсов, корнем которой является
BasicFileAttributes.Метаданные — это данные о данных. В контексте файловой системы это не содержимое файла, а его характеристики: размер, время создания, права доступа, владелец и т.д.
Интерфейс
BasicFileAttributes (пакет java.nio.file.attribute) предоставляет базовый набор атрибутов, общий для всех поддерживаемых JVM файловых систем. Для платформоспецифичных атрибутов существуют расширения: PosixFileAttributes (Unix/Linux/macOS) и DosFileAttributes (Windows). Эта архитектура следует принципу ISP (Interface Segregation Principle, принцип разделения интерфейса): общий интерфейс содержит только универсальные атрибуты, а специфичные вынесены в подинтерфейсы.Что такое атрибуты файла
Атрибуты файла делятся на несколько категорий:
Базовые атрибуты (
BasicFileAttributes) — доступны на всех платформах:Размер файла (
size) — число байтов, занимаемых файлом. Для директорий это платформозависимо и обычно равно размеру записей директории, а не сумме размеров файлов внутри.Время создания (
creationTime) — момент первого создания файла. На некоторых файловых системах может не поддерживаться или совпадать с временем модификации.Время последней модификации (
lastModifiedTime) — момент последнего изменения содержимого файла.Время последнего доступа (
lastAccessTime) — момент последнего чтения файла или выполнения stat().Тип файла — флаги
isDirectory(), isRegularFile(), isSymbolicLink(), isOther().POSIX-атрибуты (
PosixFileAttributes) — специфичны для Unix-подобных систем:Владелец (
owner) — пользователь, владеющий файлом.Группа (
group) — группа пользователей, ассоциированная с файлом.Права доступа (
permissions) — набор битов rwx для владельца, группы и остальных.DOS-атрибуты (
DosFileAttributes) — специфичны для Windows:Read-only — файл доступен только для чтения.
Hidden — скрытый файл.
System — системный файл.
Archive — флаг архивации.
Временные метки в NIO.2 представлены классом
FileTime, а не устаревшим java.util.Date или long. FileTime хранит время с точностью до наносекунд и предоставляет удобные методы конвертации.Наносекунда — единица времени, равная одной миллиардной доле секунды (10^-9 с). Современные файловые системы (ext4, NTFS, APFS) хранят временные метки с наносекундной точностью, поэтому FileTime поддерживает эту гранулярность, в отличие от Date, который ограничен миллисекундами.
Чтение атрибутов: Files.readAttributes()
Files.readAttributes(Path, Class<A>
Этот метод — основной способ чтения атрибутов файла. Он принимает путь, класс интерфейса атрибутов и опции следования по символическим ссылкам. Возвращает объект, реализующий запрошенный интерфейс.
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;
public class AttributesReader {
public void printBasicAttributes(Path path) throws IOException {
// Читаем базовые атрибуты
// LinkOption.NOFOLLOW_LINKS — читаем атрибуты самой ссылки, а не цели
BasicFileAttributes attrs = Files.readAttributes(
path,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
System.out.println("Файл: " + path.getFileName());
System.out.println("Размер: " + attrs.size() + " байт");
System.out.println("Создан: " + attrs.creationTime());
System.out.println("Изменён: " + attrs.lastModifiedTime());
System.out.println("Доступ: " + attrs.lastAccessTime());
System.out.println("Директория: " + attrs.isDirectory());
System.out.println("Обычный файл: " + attrs.isRegularFile());
System.out.println("Символическая ссылка: " + attrs.isSymbolicLink());
}
}
Метод
readAttributes() выполняет один системный вызов stat() (POSIX) или GetFileAttributesEx() (Windows), который возвращает структуру со всеми базовыми метаданными. Это эффективнее, чем вызывать отдельные методы Files.size(), Files.isDirectory(), Files.getLastModifiedTime() — каждый из которых выполняет свой системный вызов.Системный вызов stat() — это вызов ядра Unix, который возвращает структуру stat с полной информацией о файле: inode, размер, права, временные метки, количество ссылок. Аналог на Windows — GetFileAttributesEx().
Различия между readAttributes() и отдельными методами Files
// Неэффективно: три системных вызова
long size = Files.size(path);
boolean isDir = Files.isDirectory(path);
FileTime modified = Files.getLastModifiedTime(path);
// Эффективно: один системный вызов
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
long size = attrs.size();
boolean isDir = attrs.isDirectory();
FileTime modified = attrs.lastModifiedTime();
При интенсивной работе с файловой системой (например, сканирование дерева из миллиона файлов) использование
readAttributes() снижает нагрузку на систему в 3–5 раз по сравнению с отдельными вызовами.#Java #для_новичков #beginner #IO #NIO #Files #BasicFileAttributes
👍4
Методы интерфейса BasicFileAttributes
size()
Возвращает размер файла в байтах как
creationTime()
Возвращает
MFT (Master File Table) — это центральная структура данных файловой системы NTFS, содержащая записи обо всех файлах и директориях тома. Каждая запись MFT хранит метаданные файла, включая временные метки.
lastModifiedTime()
Возвращает
lastAccessTime()
Возвращает
relatime (relative atime) — режим обновления времени доступа в Linux, при котором atime обновляется только если оно старше mtime, или прошло более 24 часов с последнего обновления. Это компромисс между функциональностью и производительностью.
isDirectory(), isRegularFile(), isSymbolicLink(), isOther()
Эти методы определяют тип файловой сущности:
Именованный канал (named pipe, FIFO) — это механизм межпроцессного взаимодействия в Unix, представленный как файл в файловой системе. Процессы пишут в один конец канала и читают из другого.
Сокет домена Unix (Unix domain socket) — механизм межпроцессного взаимодействия в пределах одной машины, более быстрый, чем TCP-сокеты, так как не использует сетевой стек.
Важно: эти методы не требуют отдельных системных вызовов, так как тип файла уже известен из структуры, возвращённой stat().
fileKey()
Возвращает объект, уникально идентифицирующий файл в файловой системе. На Unix это обычно пара
Жёсткая ссылка (hard link) — это дополнительное имя для существующего файла в файловой системе. Все жёсткие ссылки на файл имеют один inode и разделяют данные. Удаление одной ссылки уменьшает счётчик ссылок; файл удаляется физически только когда счётчик достигает нуля.
Пример: вывод всех атрибутов для файла
В этом примере
Представление атрибутов (file attribute view) — это интерфейс, определяющий набор операций чтения/записи для конкретного типа атрибутов.
#Java #для_новичков #beginner #IO #NIO #Files #BasicFileAttributes
size()
Возвращает размер файла в байтах как
long. Для обычных файлов это точное количество байтов данных. Для директорий значение платформозависимо: на Unix это размер блока данных директории (список записей), а не рекурсивный размер содержимого.BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
long fileSize = attrs.size();
creationTime()
Возвращает
FileTime — момент создания файла. На файловых системах, не поддерживающих время создания (например, старые версии ext2), может возвращать lastModifiedTime() как fallback. На Windows NTFS это точное время создания записи MFT.MFT (Master File Table) — это центральная структура данных файловой системы NTFS, содержащая записи обо всех файлах и директориях тома. Каждая запись MFT хранит метаданные файла, включая временные метки.
lastModifiedTime()
Возвращает
FileTime — момент последнего изменения содержимого файла. Это время обновляется при записи в файл, усечении (truncate), изменении прав доступа (на некоторых ФС) и других операциях, модифицирующих inode.lastAccessTime()
Возвращает
FileTime — момент последнего обращения к файлу. Важно: на многих Linux-системах по умолчанию atime (access time) обновляется отложенно (relatime) или вообще не обновляется (noatime) для снижения нагрузки на диск. Поэтому это поле может быть неточным или совпадать с lastModifiedTime().relatime (relative atime) — режим обновления времени доступа в Linux, при котором atime обновляется только если оно старше mtime, или прошло более 24 часов с последнего обновления. Это компромисс между функциональностью и производительностью.
isDirectory(), isRegularFile(), isSymbolicLink(), isOther()
Эти методы определяют тип файловой сущности:
isDirectory() — директория (содержит записи других файлов).isRegularFile() — обычный файл с данными.isSymbolicLink() — символическая ссылка.isOther() — всё остальное: именованные каналы (named pipes), сокеты домена Unix, блочные и символьные устройства.Именованный канал (named pipe, FIFO) — это механизм межпроцессного взаимодействия в Unix, представленный как файл в файловой системе. Процессы пишут в один конец канала и читают из другого.
Сокет домена Unix (Unix domain socket) — механизм межпроцессного взаимодействия в пределах одной машины, более быстрый, чем TCP-сокеты, так как не использует сетевой стек.
Важно: эти методы не требуют отдельных системных вызовов, так как тип файла уже известен из структуры, возвращённой stat().
fileKey()
Возвращает объект, уникально идентифицирующий файл в файловой системе. На Unix это обычно пара
(device_id, inode_number). На Windows — идентификатор файла NTFS. Может вернуть null, если файловая система не поддерживает уникальную идентификацию.Object key = attrs.fileKey();
if (key != null) {
System.out.println("Уникальный ключ файла: " + key);
}
fileKey() полезен для обнаружения жёстких ссылок (hard links): два разных пути с одинаковым fileKey() указывают на один inode и, следовательно, на одни и те же данные.Жёсткая ссылка (hard link) — это дополнительное имя для существующего файла в файловой системе. Все жёсткие ссылки на файл имеют один inode и разделяют данные. Удаление одной ссылки уменьшает счётчик ссылок; файл удаляется физически только когда счётчик достигает нуля.
Пример: вывод всех атрибутов для файла
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class FullAttributesReporter {
public void report(Path path) throws IOException {
// Определяем, какой тип атрибутов запрашивать
// Сначала пробуем POSIX (Unix/Linux/macOS)
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
reportPosixAttributes(path);
} else if (FileSystems.getDefault().supportedFileAttributeViews().contains("dos")) {
reportDosAttributes(path);
} else {
reportBasicAttributes(path);
}
}
private void reportBasicAttributes(Path path) throws IOException {
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("=== Базовые атрибуты ===");
System.out.println("Путь: " + path.toAbsolutePath());
System.out.println("Размер: " + formatSize(attrs.size()));
System.out.println("Создан: " + formatTime(attrs.creationTime()));
System.out.println("Изменён: " + formatTime(attrs.lastModifiedTime()));
System.out.println("Доступ: " + formatTime(attrs.lastAccessTime()));
System.out.println("Тип: " + resolveType(attrs));
System.out.println("Уникальный ключ: " + attrs.fileKey());
}
private void reportPosixAttributes(Path path) throws IOException {
PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class);
reportBasicAttributes(path); // POSIX extends Basic
System.out.println("=== POSIX-атрибуты ===");
System.out.println("Владелец: " + attrs.owner().getName());
System.out.println("Группа: " + attrs.group().getName());
System.out.println("Права: " + PosixFilePermissions.toString(attrs.permissions()));
}
private void reportDosAttributes(Path path) throws IOException {
DosFileAttributes attrs = Files.readAttributes(path, DosFileAttributes.class);
reportBasicAttributes(path); // DOS extends Basic
System.out.println("=== DOS-атрибуты ===");
System.out.println("Только чтение: " + attrs.isReadOnly());
System.out.println("Скрытый: " + attrs.isHidden());
System.out.println("Системный: " + attrs.isSystem());
System.out.println("Архивный: " + attrs.isArchive());
}
private String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.2f KB", bytes / 1024.0);
if (bytes < 1024L * 1024 * 1024) return String.format("%.2f MB", bytes / (1024.0 * 1024));
return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
}
private String formatTime(FileTime time) {
// FileTime.toInstant() возвращает Instant — точку на временной шкале
return time.toInstant().toString();
}
private String resolveType(BasicFileAttributes attrs) {
if (attrs.isDirectory()) return "директория";
if (attrs.isRegularFile()) return "обычный файл";
if (attrs.isSymbolicLink()) return "символическая ссылка";
return "другое";
}
public static void main(String[] args) {
try {
new FullAttributesReporter().report(Paths.get("/etc/passwd"));
} catch (IOException e) {
System.err.println("Ошибка чтения атрибутов: " + e.getMessage());
}
}
}
В этом примере
FileSystems.getDefault().supportedFileAttributeViews() возвращает множество строк — названий поддерживаемых представлений атрибутов. Проверка "posix" позволяет адаптировать код к текущей ОС без жёсткой привязки к классам.Представление атрибутов (file attribute view) — это интерфейс, определяющий набор операций чтения/записи для конкретного типа атрибутов.
PosixFileAttributeView, DosFileAttributeView, AclFileAttributeView — примеры таких представлений. Каждое представление ассоциировано с именем-строкой ("posix", "dos", "acl").#Java #для_новичков #beginner #IO #NIO #Files #BasicFileAttributes
👍3
Изменение времени модификации: Files.setLastModifiedTime()
Метод
Он предоставляет несколько фабричных методов:
Эпоха Unix (Unix epoch) — это момент времени 00:00:00 UTC 1 января 1970 года. Время в Unix-системах традиционно отсчитывается от этой точки в секундах или миллисекундах.
Универсальный метод: Files.setAttribute()
Метод
Синтаксис имени атрибута
Имя атрибута имеет формат
UserPrincipal — это интерфейс в NIO.2, представляющий пользователя или группу в файловой системе. UserPrincipalLookupService — сервис для поиска пользователей по имени.
Метод
Рефлексия (reflection) — это механизм Java, позволяющий программе исследовать и модифицировать структуру и поведение объектов во время выполнения: получать классы, методы, поля, вызывать методы по имени.
Работа с правами доступа POSIX: Files.setPosixFilePermissions()
На Unix-подобных системах права доступа к файлам управляются через POSIX-биты. Класс
Владелец:
Группа:
Остальные:
Строковое представление "rwxr-xr-x" — это стандартная нотация Unix ls -l, где каждая триада символов описывает права для владельца, группы и остальных. Дефис означает отсутствие права.
#Java #для_новичков #beginner #IO #NIO #Files #BasicFileAttributes
Метод
Files.setLastModifiedTime(Path path, FileTime time) устанавливает время последней модификации файла. Это полезно для инструментов, которые копируют файлы и хотят сохранить оригинальные временные метки, или для тестов, которым нужно симулировать старый файл.import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.io.IOException;
import java.time.Instant;
public class ModificationTimeSetter {
public void touchFile(Path path) throws IOException {
// "Touch" — обновить время модификации до текущего момента
FileTime now = FileTime.from(Instant.now());
Files.setLastModifiedTime(path, now);
}
public void setSpecificTime(Path path, Instant instant) throws IOException {
FileTime fileTime = FileTime.from(instant);
Files.setLastModifiedTime(path, fileTime);
}
public void copyTimestamps(Path source, Path target) throws IOException {
// Копируем временные метки с источника на назначение
BasicFileAttributes sourceAttrs = Files.readAttributes(
source, BasicFileAttributes.class
);
Files.setLastModifiedTime(target, sourceAttrs.lastModifiedTime());
// Время создания и доступа устанавливаются через setAttribute
Files.setAttribute(target, "basic:creationTime", sourceAttrs.creationTime());
Files.setAttribute(target, "basic:lastAccessTime", sourceAttrs.lastAccessTime());
}
}
FileTime — неизменяемый класс, поэтому его экземпляры безопасно передавать между потоками. Он предоставляет несколько фабричных методов:
FileTime.from(Instant instant) — из объекта Instant (точка на временной шкале UTC).FileTime.fromMillis(long millis) — из миллисекунд с эпохи Unix (1 января 1970).FileTime.from(long value, TimeUnit unit) — из произвольной единицы времени.Эпоха Unix (Unix epoch) — это момент времени 00:00:00 UTC 1 января 1970 года. Время в Unix-системах традиционно отсчитывается от этой точки в секундах или миллисекундах.
Универсальный метод: Files.setAttribute()
Метод
Files.setAttribute(Path path, String attribute, Object value, LinkOption... options) позволяет установить любой атрибут по его строковому имени. Это полезно, когда вы работаете с атрибутами динамически или пишете универсальный код, не привязанный к конкретному типу файловой системы.Синтаксис имени атрибута
Имя атрибута имеет формат
viewName:attributeName. Если viewName опущен, используется "basic".// Установка времени модификации через универсальный метод
Files.setAttribute(path, "basic:lastModifiedTime", FileTime.from(Instant.now()));
// Установка POSIX-прав
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-xr-x");
Files.setAttribute(path, "posix:permissions", perms);
// Установка владельца (только Unix с соответствующими правами)
UserPrincipalLookupService lookup = FileSystems.getDefault().getUserPrincipalLookupService();
UserPrincipal newOwner = lookup.lookupPrincipalByName("admin");
Files.setAttribute(path, "posix:owner", newOwner);
UserPrincipal — это интерфейс в NIO.2, представляющий пользователя или группу в файловой системе. UserPrincipalLookupService — сервис для поиска пользователей по имени.
Метод
setAttribute() использует рефлексию и динамическую диспетчеризацию: он парсит строку имени, находит соответствующее FileAttributeView, вызывает его метод записи. Это менее эффективно, чем прямой вызов специализированного метода (например, Files.setPosixFilePermissions()), но предоставляет гибкость при работе с неизвестными заранее типами атрибутов.Рефлексия (reflection) — это механизм Java, позволяющий программе исследовать и модифицировать структуру и поведение объектов во время выполнения: получать классы, методы, поля, вызывать методы по имени.
Работа с правами доступа POSIX: Files.setPosixFilePermissions()
На Unix-подобных системах права доступа к файлам управляются через POSIX-биты. Класс
PosixFilePermissions предоставляет удобные методы для работы с ними.import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;
import java.util.Set;
public class PosixPermissionManager {
public void setStrictPermissions(Path path) throws IOException {
// rw------- — только владелец может читать и писать
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
Files.setPosixFilePermissions(path, perms);
}
public void setExecutable(Path path) throws IOException {
// Добавляем право на выполнение для владельца
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(path);
perms.add(PosixFilePermission.OWNER_EXECUTE);
Files.setPosixFilePermissions(path, perms);
}
public void setSharedReadOnly(Path path) throws IOException {
// r--r--r-- — все могут читать, никто не может писать
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("r--r--r--");
Files.setPosixFilePermissions(path, perms);
}
public void copyPermissions(Path source, Path target) throws IOException {
// Копируем POSIX-права с одного файла на другой
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(source);
Files.setPosixFilePermissions(target, perms);
}
public void createWithPermissions(Path path, String permString) throws IOException {
Set<PosixFilePermission> perms = PosixFilePermissions.fromString(permString);
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
// createFile с FileAttribute устанавливает права атомарно при создании
Files.createFile(path, attr);
}
}
PosixFilePermission — это enum, определяющий девять возможных битов прав:Владелец:
OWNER_READ, OWNER_WRITE, OWNER_EXECUTEГруппа:
GROUP_READ, GROUP_WRITE, GROUP_EXECUTEОстальные:
OTHERS_READ, OTHERS_WRITE, OTHERS_EXECUTEСтроковое представление "rwxr-xr-x" — это стандартная нотация Unix ls -l, где каждая триада символов описывает права для владельца, группы и остальных. Дефис означает отсутствие права.
#Java #для_новичков #beginner #IO #NIO #Files #BasicFileAttributes
👍4
Пример: установка времени модификации файла в текущий момент
Команда
Путь байтов в памяти JVM при работе с атрибутами
Чтение атрибутов: путь данных
При вызове
Объект
Выполняется системный вызов
JNI-код JVM копирует поля структуры из нативной памяти в объекты Java. Создаётся экземпляр внутреннего класса, реализующего
Поля объекта атрибутов содержат примитивы (
Объект
Если вы сохраняете атрибуты в долгоживущую коллекцию (например, строите карту
Запись атрибутов: путь данных
При
Объект
Выполняется системный вызов
Ядро ОС обновляет соответствующие поля в inode/MFT. Это операция только над метаданными; блоки данных файла не затрагиваются.
Никаких новых объектов в куче JVM не создаётся. Метод возвращает
POSIX-права: путь данных
При
Множество
JVM преобразует множество разрешений в целое число — битовую маску POSIX-прав. Эта маска передаётся в нативный код.
Выполняется системный вызов
Объект
ClassLoader — это компонент JVM, отвечающий за динамическую загрузку классов в память. Каждый загруженный класс хранится в Metaspace и связан с ClassLoader'ом. Когда ClassLoader становится недостижимым для GC, все его классы и связанные с ними структуры Metaspace могут быть освобождены.
Оптимизация: массовое чтение атрибутов и давление на GC
При сканировании больших файловых деревьев частая ошибка — хранение всех атрибутов в памяти:
Каждый объект
Оптимизированный подход — извлекать только нужные поля и не хранить объекты атрибутов:
Ещё лучше — использовать
#Java #для_новичков #beginner #IO #NIO #Files #BasicFileAttributes
import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.io.IOException;
import java.time.Instant;
public class FileToucher {
public void touch(Path path) throws IOException {
if (Files.notExists(path)) {
// Если файла нет — создаём пустой файл
// и время модификации будет равно времени создания (сейчас)
Files.createFile(path);
System.out.println("Файл создан: " + path);
return;
}
// Файл существует — обновляем только время модификации
FileTime now = FileTime.from(Instant.now());
Files.setLastModifiedTime(path, now);
// Проверяем, что установилось
FileTime verify = Files.getLastModifiedTime(path);
if (verify.compareTo(now) >= 0) {
System.out.println("Время модификации обновлено: " + verify);
} else {
throw new IOException("Не удалось обновить время модификации");
}
}
public void touchWithAccessTime(Path path) throws IOException {
// Обновляем и время модификации, и время доступа
FileTime now = FileTime.from(Instant.now());
Files.setAttribute(path, "basic:lastModifiedTime", now);
Files.setAttribute(path, "basic:lastAccessTime", now);
// Проверяем через readAttributes
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("mtime: " + attrs.lastModifiedTime());
System.out.println("atime: " + attrs.lastAccessTime());
}
public static void main(String[] args) {
try {
Path file = Paths.get("/tmp/test_touch.txt");
new FileToucher().touch(file);
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
Команда
touch в Unix работает именно так: если файла нет — создаёт пустой, если есть — обновляет временные метки. Важно, что Files.setLastModifiedTime() не изменяет содержимое файла, только метаданные inode.Путь байтов в памяти JVM при работе с атрибутами
Чтение атрибутов: путь данных
При вызове
Files.readAttributes(path, BasicFileAttributes.class):Объект
Path находится в куче (Heap). JVM извлекает его строковое представление и кодирует в массив байтов пути в нативной памяти через JNI.Выполняется системный вызов
stat() (POSIX) или GetFileAttributesEx() (Windows). Ядро ОС читает inode/MFT-запись и возвращает структуру с метаданными в нативную память ядра.JNI-код JVM копирует поля структуры из нативной памяти в объекты Java. Создаётся экземпляр внутреннего класса, реализующего
BasicFileAttributes (например, UnixFileAttributes в OpenJDK). Этот объект создаётся в Young Generation, в области Eden.Поля объекта атрибутов содержат примитивы (
long для размера, long для наносекунд времени) и объекты FileTime. Каждый вызов creationTime(), lastModifiedTime(), lastAccessTime() возвращает новый объект FileTime или кэшированный экземпляр — это зависит от реализации JVM. В OpenJDK FileTime создаётся лениво при первом вызове геттера и кэшируется в поле объекта атрибутов.Объект
BasicFileAttributes — лёгкий: он содержит несколько long-полей и ссылки на FileTime. Если вы читаете атрибуты в цикле (например, при сканировании директории из 100 000 файлов), каждая итерация создаёт новый объект атрибутов в Eden. Эти объекты становятся мусором сразу после обработки файла, если вы не сохраняете ссылку. Minor GC эффективно собирает их, так как они не переживают цикла.Если вы сохраняете атрибуты в долгоживущую коллекцию (например, строите карту
Map<Path, BasicFileAttributes> для всех файлов проекта), объекты атрибутов переходят в Old Generation через Survivor Space. При большом количестве файлов это может существенно увеличить размер Old Generation и вызвать Major GC (Full GC) раньше, чем ожидалось.Запись атрибутов: путь данных
При
Files.setLastModifiedTime(path, fileTime):Объект
FileTime хранит время как long в наносекундах (или секундах + наносекунды). Он находится в куче. JVM извлекает примитивное значение и передаёт его в нативный код.Выполняется системный вызов
utimensat() (современный POSIX, поддерживает наносекунды) или utime() (устаревший, точность до секунд). На Windows — SetFileTime().Ядро ОС обновляет соответствующие поля в inode/MFT. Это операция только над метаданными; блоки данных файла не затрагиваются.
Никаких новых объектов в куче JVM не создаётся. Метод возвращает
void. Объект FileTime, переданный как параметр, остаётся в куче до тех пор, пока на него есть ссылка. Если это локальная переменная — он собирается при следующей Minor GC.POSIX-права: путь данных
При
Files.setPosixFilePermissions(path, permissions):Множество
Set<PosixFilePermission> — это объект в куче, содержащий ссылки на enum-константы. Enum-константы в Java создаются один раз при загрузке класса и хранятся в Metaspace (вместе с классом PosixFilePermission). Само множество — это объект HashSet или EnumSet в куче.JVM преобразует множество разрешений в целое число — битовую маску POSIX-прав. Эта маска передаётся в нативный код.
Выполняется системный вызов
chmod() (POSIX) или SetFileSecurity() (Windows, если используется ACL-эмуляция). Ядро обновляет биты прав в inode.Объект
Set<PosixFilePermission> становится мусором после выхода из метода, если он не сохранён. Minor GC собирает его. Enum-константы остаются в Metaspace навсегда (до выгрузки класса), так как Metaspace не подлежит обычной сборке мусора в полном смысле — классы выгружаются только при выгрузке ClassLoader'а.ClassLoader — это компонент JVM, отвечающий за динамическую загрузку классов в память. Каждый загруженный класс хранится в Metaspace и связан с ClassLoader'ом. Когда ClassLoader становится недостижимым для GC, все его классы и связанные с ними структуры Metaspace могут быть освобождены.
Оптимизация: массовое чтение атрибутов и давление на GC
При сканировании больших файловых деревьев частая ошибка — хранение всех атрибутов в памяти:
// Проблемный код: храним все атрибуты в памяти
Map<Path, BasicFileAttributes> cache = new HashMap<>();
Files.walk(root).forEach(p -> {
try {
cache.put(p, Files.readAttributes(p, BasicFileAttributes.class));
} catch (IOException e) { /* ignore */ }
});
// При миллионе файлов cache содержит миллион объектов BasicFileAttributes в Old Generation
Каждый объект
BasicFileAttributes в OpenJDK занимает примерно 40–56 байт (зависит от архитектуры и JVM). Для миллиона файлов это 40–56 МБ только на объекты атрибутов, плюс накладные расходы HashMap (примерно в 2 раза больше). Это может привести к Major GC или OOM.Оптимизированный подход — извлекать только нужные поля и не хранить объекты атрибутов:
// Оптимизированный код: извлекаем только размер и mtime, храним примитивы
Map<Path, Long> sizeCache = new HashMap<>();
Files.walk(root).forEach(p -> {
try {
BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class);
sizeCache.put(p, attrs.size()); // только long, лёгкий объект
} catch (IOException e) { /* ignore */ }
});
// Объект attrs становится мусором сразу после итерации
Ещё лучше — использовать
Files.newDirectoryStream() и читать атрибуты порциями, не загружая всё дерево в память:// Обход с минимальным потреблением памяти
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path entry : stream) {
BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class);
process(entry, attrs.size(), attrs.lastModifiedTime());
// attrs собирается GC сразу после process()
}
}
#Java #для_новичков #beginner #IO #NIO #Files #BasicFileAttributes
👍3
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 2. Современный NIO.2 (java.nio.file)
Files.list() и Files.walk() — обход содержимого директорий
Работа с файловой системой в Java NIO.2 предоставляет два ключевых метода для обхода директорий:
Stream API — это фреймворк в Java для обработки последовательностей элементов в функциональном стиле. Он поддерживает ленивые (lazy) промежуточные операции (filter, map, sorted) и энергичные (eager) терминальные операции (count, collect, forEach). Stream не хранит данные, а описывает конвейер вычислений.
Files.list(Path) — содержимое директории (не рекурсивно)
Метод
Важно:
Файловый дескриптор (file descriptor) — это целочисленный идентификатор, который ядро ОС присваивает открытому файлу или ресурсу. В Unix это индекс в таблице открытых файлов процесса. JVM не знает о дескрипторах ОС — GC управляет только памятью кучи, не ресурсами операционной системы.
Фильтрация по расширению: пример вывода всех .txt файлов
В оптимизированной версии
Record — это тип класса в Java, введённый в Java 16 (preview в 14-15), который автоматически генерирует конструктор, геттеры, equals(), hashCode() и toString() на основе объявленных компонентов. record FileInfo(Path name, long size, FileTime modified) создаёт неизменяемый (immutable) класс данных.
Псевдокод: внутренняя работа Files.list()
Ключевой момент:
Files.walk(Path) — рекурсивный обход дерева
Метод
Files.walk(Path, int maxDepth) — ограничение глубины
Метод
#Java #для_новичков #beginner #IO #NIO #Files #Fileslist #Fileswalk
Глава 2. Современный NIO.2 (java.nio.file)
Files.list() и Files.walk() — обход содержимого директорий
Работа с файловой системой в Java NIO.2 предоставляет два ключевых метода для обхода директорий:
Files.list() для непосредственного содержимого и Files.walk() для рекурсивного обхода дерева. Оба метода возвращают Stream<Path>, что позволяет использовать всю мощь Stream API для фильтрации, преобразования и агрегации данных. Однако за этой элегантностью скрываются важные нюансы производительности, управления ресурсами и потребления памяти, которые критичны при работе с большими файловыми деревьями.Stream API — это фреймворк в Java для обработки последовательностей элементов в функциональном стиле. Он поддерживает ленивые (lazy) промежуточные операции (filter, map, sorted) и энергичные (eager) терминальные операции (count, collect, forEach). Stream не хранит данные, а описывает конвейер вычислений.
Files.list(Path) — содержимое директории (не рекурсивно)
Метод
Files.list(Path dir) возвращает Stream<Path>, содержащий прямые потомки указанной директории. Поток ленивый: записи директории читаются по мере запроса элементов из потока, а не все сразу.public class DirectoryLister {
public void listDirectory(Path dir) throws IOException {
// Stream<Path> — ленивый поток, записи читаются по требованию
try (Stream<Path> stream = Files.list(dir)) {
stream.forEach(path -> {
String type = Files.isDirectory(path) ? "[DIR]" : "[FILE]";
System.out.println(type + " " + path.getFileName());
});
}
}
}Важно:
Files.list() возвращает поток, который держит открытым файловый дескриптор директории до тех пор, пока поток не закрыт. Поэтому обязательно использование try-with-resources — иначе дескриптор утечёт на уровне ОС, даже если объект Stream будет собран GC.Файловый дескриптор (file descriptor) — это целочисленный идентификатор, который ядро ОС присваивает открытому файлу или ресурсу. В Unix это индекс в таблице открытых файлов процесса. JVM не знает о дескрипторах ОС — GC управляет только памятью кучи, не ресурсами операционной системы.
Фильтрация по расширению: пример вывода всех .txt файлов
public class TxtFileFinder {
public void findTxtFiles(Path dir) throws IOException {
try (Stream<Path> stream = Files.list(dir)) {
stream.filter(Files::isRegularFile) // только файлы, не директории
.filter(this::hasTxtExtension) // фильтр по расширению
.forEach(this::printFileInfo);
}
}
private boolean hasTxtExtension(Path path) {
String fileName = path.getFileName().toString();
// endsWith чувствителен к регистру — на case-sensitive ФС это корректно
return fileName.endsWith(".txt");
}
private void printFileInfo(Path path) {
try {
long size = Files.size(path);
FileTime modified = Files.getLastModifiedTime(path);
System.out.printf("%s (%d bytes, modified: %s)%n",
path.getFileName(), size, modified);
} catch (IOException e) {
System.err.println("Ошибка чтения атрибутов: " + path.getFileName());
}
}
// Альтернатива: чтение атрибутов за один вызов для каждого файла
public void findTxtFilesOptimized(Path dir) throws IOException {
try (Stream<Path> stream = Files.list(dir)) {
stream.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".txt"))
.map(p -> {
try {
BasicFileAttributes attrs = Files.readAttributes(
p, BasicFileAttributes.class
);
return new FileInfo(p.getFileName(), attrs.size(),
attrs.lastModifiedTime());
} catch (IOException e) {
return null;
}
})
.filter(Objects::nonNull)
.forEach(System.out::println);
}
}
private record FileInfo(Path name, long size, FileTime modified) {}
}В оптимизированной версии
readAttributes() читает все атрибуты за один системный вызов вместо отдельных Files.size() и Files.getLastModifiedTime(). Однако map() в Stream API требует, чтобы лямбда не выбрасывала checked-исключения, поэтому приходится оборачивать в try-catch внутри лямбды.Record — это тип класса в Java, введённый в Java 16 (preview в 14-15), который автоматически генерирует конструктор, геттеры, equals(), hashCode() и toString() на основе объявленных компонентов. record FileInfo(Path name, long size, FileTime modified) создаёт неизменяемый (immutable) класс данных.
Псевдокод: внутренняя работа Files.list()
функция list(Path dir) -> Stream<Path>:
открыть файловый дескриптор директории dir (системный вызов opendir)
создать DirectoryStream для чтения записей
создать Stream, который при запросе следующего элемента:
вызывает DirectoryStream.readNextEntry()
если запись есть — возвращает Path(entry_name)
если записей больше нет — закрывает дескриптор и завершает поток
зарегистрировать onClose для Stream, который закрывает дескриптор
вернуть Stream
Ключевой момент:
DirectoryStream читает записи порциями из буфера ядра. Размер буфера зависит от файловой системы и ОС (обычно 4–32 КБ). Это означает, что для директории с миллионом файлов Files.list() не загружает все имена в память сразу — оно читает их порциями по мере итерации потока.Files.walk(Path) — рекурсивный обход дерева
Метод
Files.walk(Path start) выполняет обход дерева файловой системы, начиная с указанного пути, и возвращает Stream<Path> со всеми посещёнными путями. Обход выполняется в порядке pre-order depth-first (прямой порядок, глубина вперёд): сначала обрабатывается родительский узел, затем его потомки.public void walkDirectory(Path root) throws IOException {
try (Stream<Path> stream = Files.walk(root)) {
stream.forEach(path -> {
int depth = root.relativize(path).getNameCount();
String indent = " ".repeat(depth);
System.out.println(indent + path.getFileName());
});
}
}Files.walk(Path, int maxDepth) — ограничение глубины
Метод
Files.walk(Path start, int maxDepth) ограничивает глубину обхода. maxDepth = 0 означает только сам start. maxDepth = 1 — start и его прямые потомки (аналогично Files.list() плюс сам корень).public void walkWithDepthLimit(Path root) throws IOException {
// Обходим только 2 уровня вглубь
try (Stream<Path> stream = Files.walk(root, 2)) {
stream.filter(Files::isRegularFile)
.forEach(p -> System.out.println(p.toAbsolutePath()));
}
}#Java #для_новичков #beginner #IO #NIO #Files #Fileslist #Fileswalk
👍5
Порядок обхода и символические ссылки
По умолчанию
Это может привести к:
Бесконечному циклу, если символическая ссылка указывает на предка в дереве (например,
Обходу файловой системы за пределами ожидаемого дерева (например, ссылка на
Для безопасного обхода используйте
FileVisitOption — это enum, определяющий опции обхода дерева.
Псевдокод: внутренняя работа Files.walk()
Важно:
#Java #для_новичков #beginner #IO #NIO #Files #Fileslist #Fileswalk
По умолчанию
Files.walk() следует по символическим ссылкам (follows symlinks). Это означает, что если директория содержит символическую ссылку на другую директорию, обход зайдёт внутрь этой целевой директории. Это может привести к:
Бесконечному циклу, если символическая ссылка указывает на предка в дереве (например,
ln -s .. loop).Обходу файловой системы за пределами ожидаемого дерева (например, ссылка на
/etc).Для безопасного обхода используйте
FileVisitOption.FOLLOW_LINKS явно, только если это нужно, или обходите ссылки как файлы:public class SafeWalker {
// Безопасный обход: символические ссылки обрабатываются как файлы, не следуем по ним
public void walkNoFollowLinks(Path root) throws IOException {
// По умолчанию walk НЕ следует по ссылкам — это поведение по умолчанию
// Но явно можно указать:
try (Stream<Path> stream = Files.walk(root)) {
stream.forEach(path -> {
if (Files.isSymbolicLink(path)) {
System.out.println("[LINK] " + path.getFileName());
}
});
}
}
// Обход с явным следованием по ссылкам (осторожно!)
public void walkFollowingLinks(Path root) throws IOException {
try (Stream<Path> stream = Files.walk(root, FileVisitOption.FOLLOW_LINKS)) {
stream.forEach(System.out::println);
}
}
}FileVisitOption — это enum, определяющий опции обхода дерева.
FOLLOW_LINKS указывает, что символические ссылки на директории должны трактоваться как директории и обходиться рекурсивно. Без этой опции ссылки трактуются как обычные файлы.Псевдокод: внутренняя работа Files.walk()
функция walk(Path start, int maxDepth) -> Stream<Path>:
создать очередь (Deque) для обхода в ширину/глубину
поместить start в очередь с глубиной 0
создать Stream, который при запросе элемента:
извлекает следующий путь из очереди
возвращает его как элемент Stream
если это директория И глубина < maxDepth:
открыть DirectoryStream для этой директории
для каждой записи:
поместить entry в очередь с глубиной + 1
зарегистрировать onClose для закрытия всех открытых DirectoryStream
вернуть Stream
Важно:
Files.walk() открывает DirectoryStream для каждой директории в дереве. Все эти потоки держат открытые файловые дескрипторы до закрытия основного Stream<Path>. Поэтому try-with-resources обязателен — иначе при аварийном завершении обхода дескрипторы останутся открытыми.#Java #для_новичков #beginner #IO #NIO #Files #Fileslist #Fileswalk
👍5
Параллельный обход: предостережения
Порядок обхода непредсказуем: элементы обрабатываются в произвольном порядке.
Конкурентный доступ к файловой системе: множественные потоки одновременно открывают директории, что может привести к contention (конкуренции) на уровне ОС.
Символические ссылки: при параллельном обходе с
Ошибки обхода: если один поток получает
Рекомендуется использовать параллельный обход только для больших деревьев на быстрых локальных дисках (SSD, NVMe) и только если операция для каждого файла тяжёлая (например, вычисление хэша, а не просто чтение размера).
ForkJoinPool — это пул потоков в Java, реализующий алгоритм work-stealing (воровство задач). Потоки, завершившие свои задачи, "воруют" задачи из очередей других потоков, что обеспечивает равномерную загрузку. commonPool() — это общий пул, используемый по умолчанию для параллельных Stream.
Осторожно: обход может быть дорогим для больших деревьев
Проблема 1: Потребление памяти
Проблема 2: Файловые дескрипторы
Каждая открытая директория в обходе дерева держит файловый дескриптор. Для глубокого дерева с тысячами директорий это может исчерпать лимит дескрипторов процесса (ulimit -n на Unix, обычно 1024–65536). Если лимит исчерпан, последующие
Проблема 3: Время обхода
Обход дерева из миллионов файлов может занять минуты.
Проблема 4: Сортировка и материализация
Если вы применяете
Альтернатива: Files.walkFileTree() для контролируемого обхода
Для сценариев, где требуется точный контроль над обходом (пропуск поддеревьев, обработка ошибок, прерывание), используйте
#Java #для_новичков #beginner #IO #NIO #Files #Fileslist #Fileswalk
stream.parallel() распределяет обход по потокам ForkJoinPool.commonPool(). Это может ускорить обход на многопроцессорных системах, но есть риски:Порядок обхода непредсказуем: элементы обрабатываются в произвольном порядке.
Конкурентный доступ к файловой системе: множественные потоки одновременно открывают директории, что может привести к contention (конкуренции) на уровне ОС.
Символические ссылки: при параллельном обходе с
FOLLOW_LINKS риск зацикливания возрастает.Ошибки обхода: если один поток получает
AccessDeniedException, это не прерывает другие потоки, но обработка ошибок усложняется.Рекомендуется использовать параллельный обход только для больших деревьев на быстрых локальных дисках (SSD, NVMe) и только если операция для каждого файла тяжёлая (например, вычисление хэша, а не просто чтение размера).
ForkJoinPool — это пул потоков в Java, реализующий алгоритм work-stealing (воровство задач). Потоки, завершившие свои задачи, "воруют" задачи из очередей других потоков, что обеспечивает равномерную загрузку. commonPool() — это общий пул, используемый по умолчанию для параллельных Stream.
Осторожно: обход может быть дорогим для больших деревьев
Проблема 1: Потребление памяти
Files.walk() использует внутреннюю очередь для хранения путей, ожидающих обхода. Для сильно ветвящегося дерева (например, директория с миллионом файлов на первом уровне) эта очередь может занять значительный объём памяти. Хотя поток ленивый, внутренний буфер между DirectoryStream и Stream может накапливать пути.Проблема 2: Файловые дескрипторы
Каждая открытая директория в обходе дерева держит файловый дескриптор. Для глубокого дерева с тысячами директорий это может исчерпать лимит дескрипторов процесса (ulimit -n на Unix, обычно 1024–65536). Если лимит исчерпан, последующие
opendir() вызовы вернут EMFILE (too many open files), что JVM преобразует в IOException.Проблема 3: Время обхода
Обход дерева из миллионов файлов может занять минуты.
Stream не предоставляет механизма прерывания (cancellation) стандартными средствами. Прерывание потока через Thread.interrupt() не прерывает внутренний обход — он продолжит открывать директории и читать записи.Проблема 4: Сортировка и материализация
Если вы применяете
sorted() к результату Files.walk(), весь поток материализуется в список в памяти перед сортировкой. Для большого дерева это может привести к OutOfMemoryError.// ОПАСНО: материализует все пути в память
Files.walk(root).sorted().forEach(...); // OOM при миллионе файлов
// Безопасно: обход без сортировки
Files.walk(root).forEach(...); // ленивый, память ограничена буфером
Альтернатива: Files.walkFileTree() для контролируемого обхода
Для сценариев, где требуется точный контроль над обходом (пропуск поддеревьев, обработка ошибок, прерывание), используйте
Files.walkFileTree() с FileVisitor:import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;
public class ControlledTreeWalker implements FileVisitor<Path> {
private final AtomicLong totalSize = new AtomicLong(0);
private final long maxSizeToProcess; // прервать после достижения лимита
private volatile boolean cancelled = false;
public ControlledTreeWalker(long maxSize) {
this.maxSizeToProcess = maxSize;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (cancelled) {
return FileVisitResult.TERMINATE; // прервать обход
}
// Пропускаем скрытые директории
try {
if (Files.isHidden(dir)) {
return FileVisitResult.SKIP_SUBTREE; // не заходить внутрь
}
} catch (IOException e) {
// Продолжаем, если не удалось проверить
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelled) {
return FileVisitResult.TERMINATE;
}
if (attrs.isRegularFile()) {
totalSize.addAndGet(attrs.size());
if (totalSize.get() >= maxSizeToProcess) {
cancelled = true;
return FileVisitResult.TERMINATE;
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
// Логируем ошибку, но продолжаем обход
System.err.println("Ошибка доступа к файлу: " + file + " — " + exc.getMessage());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
if (exc != null) {
System.err.println("Ошибка обхода директории: " + dir + " — " + exc.getMessage());
}
return FileVisitResult.CONTINUE;
}
public long getTotalSize() {
return totalSize.get();
}
public static long calculateSizeWithLimit(Path root, long limit) throws IOException {
ControlledTreeWalker walker = new ControlledTreeWalker(limit);
Files.walkFileTree(root, walker);
return walker.getTotalSize();
}
}
#Java #для_новичков #beginner #IO #NIO #Files #Fileslist #Fileswalk
👍6
AtomicLong — это класс в пакете java.util.concurrent.atomic, предоставляющий атомарные операции над long без блокировок (lock-free). Он использует примитивы CAS (Compare-And-Swap) на уровне процессора для обеспечения потокобезопасности. В отличие от synchronized, atomic-классы не требуют захвата монитора и более эффективны при высокой конкуренции.
FileVisitResult — это enum, определяющий действие после обработки узла дерева:
Путь байтов в памяти JVM при обходе директорий
Files.list(): путь данных
При вызове
Объект
Выполняется системный вызов
Если забыть закрыть поток, объект
При итерации по
Если обход короткий (несколько сотен файлов), все объекты
Tenuring — это процесс в JVM, при котором объекты, пережившие несколько циклов Minor GC, перемещаются из Survivor Space в Old Generation. Порог (tenuring threshold) настраивается через опцию
Files.walk(): путь данных
При
Метод создаёт внутренний
Для каждой директории в дереве
Когда
Очередь
Каждый объект
При фильтрации через
GC churn — это ситуация, когда большое количество объектов быстро создаётся и уничтожается, вызывая частые циклы сборки мусора. Это снижает производительность приложения, так как потоки исполнения приостанавливаются (stop-the-world паузы) для работы GC.
Оптимизация: избегаем лишних объектов
Для снижения давления на GC при массовом обходе:
В этом коде нет объектов
Boxing — это процесс автоматической упаковки примитивного типа в соответствующий объект-обёртку (например, long в Long). В Stream API mapToLong() возвращает LongStream, который работает с примитивами и избегает boxing. Но map() с лямбдой, возвращающей Long, вызывает boxing на каждой итерации.
#Java #для_новичков #beginner #IO #NIO #Files #Fileslist #Fileswalk
FileVisitResult — это enum, определяющий действие после обработки узла дерева:
CONTINUE — продолжить обход.TERMINATE — прервать обход полностью.SKIP_SUBTREE — не заходить в текущую директорию (для preVisitDirectory).SKIP_SIBLINGS — пропустить остальные элементы текущей директории.Files.walkFileTree() не использует Stream API и не материализует дерево в памяти. Он обходит рекурсивно, вызывая callback'и, и управляет стеком вызовов самостоятельно. Это более предсказуемо по памяти, но менее гибко для функциональных преобразований (filter, map, reduce).Путь байтов в памяти JVM при обходе директорий
Files.list(): путь данных
При вызове
Files.list(dir):Объект
Path (директория) находится в куче (Heap). JVM извлекает его строковое представление и кодирует в байты пути в нативной памяти.Выполняется системный вызов
opendir() (POSIX) или FindFirstFile() (Windows). Ядро ОС открывает директорию, выделяет файловый дескриптор и возвращает его в пространство процесса. Дескриптор — это целое число, хранящееся в нативной памяти JVM (не в куче), в структуре DirectoryStream.DirectoryStream — это объект в куче, содержащий ссылку на нативный дескриптор. Он создаётся в Young Generation, в Eden. При закрытии потока (вызов close() или try-with-resources) выполняется системный вызов closedir(), который освобождает дескриптор на уровне ОС.Если забыть закрыть поток, объект
DirectoryStream становится мусором и собирается Minor GC (или Major GC, если он пережил несколько циклов). Однако GC не знает о нативном дескрипторе — он освобождает только память кучи. Дескриптор остаётся открытым до завершения процесса или исчерпания лимита дескрипторов. Это классическая утечка нативных ресурсов.При итерации по
Stream<Path> каждая запись директории читается через readdir() (POSIX) или FindNextFile() (Windows). Запись возвращается как структура в нативной памяти, из которой JVM извлекает имя файла и создаёт новый объект Path в куче. Этот Path создаётся в Young Generation.Если обход короткий (несколько сотен файлов), все объекты
Path создаются в Eden и собираются при следующей Minor GC. Если обход длительный (миллионы файлов), объекты Path могут пережить несколько циклов Minor GC и мигрировать в Survivor Space, а затем в Old Generation через механизм tenuring (старение).Tenuring — это процесс в JVM, при котором объекты, пережившие несколько циклов Minor GC, перемещаются из Survivor Space в Old Generation. Порог (tenuring threshold) настраивается через опцию
-XX:MaxTenuringThreshold (по умолчанию 15 в G1 GC).Files.walk(): путь данных
При
Files.walk(root):Метод создаёт внутренний
FileTreeWalker — объект в куче, который управляет обходом дерева. Он содержит очередь (ArrayDeque<Path>) для хранения путей, ожидающих обхода.Для каждой директории в дереве
FileTreeWalker открывает DirectoryStream, что порождает новый файловый дескриптор. Все эти дескрипторы остаются открытыми до закрытия основного Stream.Когда
Stream запрашивает следующий элемент, FileTreeWalker извлекает путь из очереди. Если это директория и глубина позволяет, он открывает её DirectoryStream, читает записи и добавляет их в очередь. Каждый добавленный путь — это новый объект Path в куче.Очередь
FileTreeWalker может накапливать пути. Для дерева с фактором ветвления B (среднее число потомков на узел) и глубиной D максимальный размер очереди примерно B * D. Для B=100 и D=10 это 1000 объектов Path — незначительно. Но для B=10000 (директория с 10000 файлов) на первом уровне очередь может содержать 10000 объектов Path.Каждый объект
Path в OpenJDK (реализация UnixPath или WindowsPath) хранит массив строк-сегментов и ссылку на FileSystem. Размер объекта — примерно 40–80 байт плюс массив сегментов. Для 10000 файлов это 400–800 КБ только на объекты Path — приемлемо для современных JVM. Но для миллиона файлов на одном уровне — 40–80 МБ, что может вызвать давление на Old Generation.При фильтрации через
stream.filter(Files::isRegularFile) каждый отфильтрованный элемент (директория) всё равно создаётся как объект Path, но не проходит дальше по конвейеру. Объект становится мусором и собирается Minor GC. Однако если фильтрация редко пропускает элементы (например, ищем один файл среди миллиона), большинство объектов Path создаются и тут же уничтожаются — это создаёт высокую нагрузку на Minor GC (GC churn).GC churn — это ситуация, когда большое количество объектов быстро создаётся и уничтожается, вызывая частые циклы сборки мусора. Это снижает производительность приложения, так как потоки исполнения приостанавливаются (stop-the-world паузы) для работы GC.
Оптимизация: избегаем лишних объектов
Для снижения давления на GC при массовом обходе:
// Оптимизация: используем walkFileTree вместо Stream для минимизации объектов
public long fastSizeCalculation(Path root) throws IOException {
long[] total = new long[1]; // массив примитивов — нет boxing, нет объектов
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
total[0] += attrs.size(); // прямой доступ к примитиву
return FileVisitResult.CONTINUE;
}
});
return total[0];
}
В этом коде нет объектов
Stream, нет лямбд (которые компилируются в синтетические классы и требуют загрузки в Metaspace), нет boxing'а Long. Единственные объекты в куче — SimpleFileVisitor (один на весь обход) и BasicFileAttributes для каждого файла (лёгкие, собираются Minor GC). Это самый эффективный способ с точки зрения памяти и GC.Boxing — это процесс автоматической упаковки примитивного типа в соответствующий объект-обёртку (например, long в Long). В Stream API mapToLong() возвращает LongStream, который работает с примитивами и избегает boxing. Но map() с лямбдой, возвращающей Long, вызывает boxing на каждой итерации.
#Java #для_новичков #beginner #IO #NIO #Files #Fileslist #Fileswalk
👍5
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 2. Современный NIO.2 (java.nio.file)
Files.lines() и WatchService — построчное чтение и мониторинг файловой системы
Два механизма в Java NIO.2 решают противоположные задачи:
Files.lines(Path) — построчное чтение текстовых файлов
Метод
Важно:
BufferedReader — это класс в Java, который буферизует чтение символов из потока. Вместо чтения по одному символу или байту он читает блоками (обычно 8 КБ) в буфер в памяти, а затем отдаёт символы из буфера. Это снижает количество системных вызовов read() с O(N) до O(N / buffer_size).
Files.lines(Path, Charset) — с указанием кодировки
Если кодировка не указана, используется
Кодировка (charset, character encoding) — это схема сопоставления байтовых последовательностей символам текста. UTF-8 — переменная длина (1–4 байта на символ), обратно совместима с ASCII. ISO-8859-1 (Latin-1) — фиксированная 1 байт на символ, покрывает западноевропейские языки.
Ленивость и потребление памяти
В отличие от
WatchService — отслеживание изменений в файловой системе
WatchService — это сервис в NIO.2, который использует нативные механизмы ОС для мониторинга изменений в файловой системе. Вместо периодического опроса (polling) он работает по событийной модели: ОС уведомляет JVM, когда происходит зарегистрированное событие.
Polling (опрос) — это стратегия проверки состояния, при которой программа периодически запрашивает ресурс (например, проверяет время модификации файла). Это расходует CPU впустую. Событийная модель (event-driven) — противоположный подход, при котором программа ждёт уведомления от ОС и не потребляет ресурсы в ожидании.
Нативные механизмы ОС
WatchService в Java — это тонкая обёртка над нативными API:
Linux:
macOS:
Windows:
Важно:
Coalescing (слияние событий) — это оптимизация, при которой несколько последовательных событий над одним и тем же файлом объединяются в одно. Например, множественные быстрые записи в файл могут породить только одно событие ENTRY_MODIFY.
Создание WatchService
Регистрация директории
WatchKey — это объект, представляющий регистрацию директории в WatchService. Он содержит ссылку на зарегистрированный путь и очередь событий. Когда WatchKey становится недействительным (например, директория удалена), его метод isValid() возвращает false.
#Java #для_новичков #beginner #IO #NIO #Files #Files_lines #WatchService
Глава 2. Современный NIO.2 (java.nio.file)
Files.lines() и WatchService — построчное чтение и мониторинг файловой системы
Два механизма в Java NIO.2 решают противоположные задачи:
Files.lines() читает содержимое файла построчно, а WatchService наблюдает за изменениями в файловой системе без активного чтения. Первый экономит память при обработке больших текстовых файлов, второй — CPU при ожидании событий. Оба требуют строгого управления ресурсами, так как работают с нативными дескрипторами ОС, невидимыми для GC.Files.lines(Path) — построчное чтение текстовых файлов
Метод
Files.lines(Path path) возвращает Stream<String>, который лениво читает файл построчно. Каждая строка декодируется из байтов файла в символы с использованием кодировки по умолчанию (StandardCharsets.UTF_8).import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
public class LineReader {
public void readLines(Path file) throws IOException {
// Stream<String> — ленивое чтение, строки появляются по мере обхода
try (Stream<String> lines = Files.lines(file)) {
lines.forEach(line -> System.out.println(line));
}
// BufferedReader закрыт автоматически try-with-resources
}
}
Важно:
Files.lines() открывает BufferedReader под капотом, который держит файловый дескриптор открытым. Поток Stream<String> должен быть закрыт через try-with-resources или явный close(). Если поток не закрыт, дескриптор останется открытым даже после того, как Stream будет собран GC.BufferedReader — это класс в Java, который буферизует чтение символов из потока. Вместо чтения по одному символу или байту он читает блоками (обычно 8 КБ) в буфер в памяти, а затем отдаёт символы из буфера. Это снижает количество системных вызовов read() с O(N) до O(N / buffer_size).
Files.lines(Path, Charset) — с указанием кодировки
import java.nio.charset.StandardCharsets;
public void readWithEncoding(Path file) throws IOException {
// Явное указание кодировки — обязательно для файлов не в UTF-8
try (Stream<String> lines = Files.lines(file, StandardCharsets.ISO_8859_1)) {
lines.forEach(System.out::println);
}
}
Если кодировка не указана, используется
Charset.defaultCharset(), которая зависит от ОС и локали. На Linux обычно UTF-8, на Windows — Windows-1252 или CP1251. Неявное использование дефолтной кодировки — распространённый источник багов при переносе приложения между платформами.Кодировка (charset, character encoding) — это схема сопоставления байтовых последовательностей символам текста. UTF-8 — переменная длина (1–4 байта на символ), обратно совместима с ASCII. ISO-8859-1 (Latin-1) — фиксированная 1 байт на символ, покрывает западноевропейские языки.
Ленивость и потребление памяти
Files.lines() — ленивый поток. Строка читается из файла только когда терминальная операция запрашивает следующий элемент. Это позволяет обрабатывать файлы любого размера с константным потреблением памяти:public void processHugeFile(Path file) throws IOException {
try (Stream<String> lines = Files.lines(file)) {
// Фильтрация и подсчёт — каждая строка обрабатывается и отбрасывается
long errorCount = lines.filter(line -> line.contains("ERROR"))
.count();
System.out.println("Ошибок: " + errorCount);
// Память не зависит от размера файла — только от длины одной строки
}
}В отличие от
Files.readAllLines(), который загружает весь файл в List<String> (потенциально гигабайты в куче), Files.lines() держит в памяти только текущую строку и буфер BufferedReader (8 КБ).WatchService — отслеживание изменений в файловой системе
WatchService — это сервис в NIO.2, который использует нативные механизмы ОС для мониторинга изменений в файловой системе. Вместо периодического опроса (polling) он работает по событийной модели: ОС уведомляет JVM, когда происходит зарегистрированное событие.
Polling (опрос) — это стратегия проверки состояния, при которой программа периодически запрашивает ресурс (например, проверяет время модификации файла). Это расходует CPU впустую. Событийная модель (event-driven) — противоположный подход, при котором программа ждёт уведомления от ОС и не потребляет ресурсы в ожидании.
Нативные механизмы ОС
WatchService в Java — это тонкая обёртка над нативными API:
Linux:
inotify — система уведомлений о событиях файловой системы на уровне ядра. Каждый WatchService создаёт экземпляр inotify через системный вызов inotify_init().macOS:
FSEvents — фреймворк уведомлений о изменениях файловой системы. Работает на уровне всей ФС, а не отдельных директорий, что даёт другую семантику событий.Windows:
ReadDirectoryChangesW — WinAPI функция для асинхронного чтения изменений директории.Важно:
WatchService не гарантирует доставку всех событий и не гарантирует порядок. Некоторые события могут быть пропущены при высокой нагрузке, и события могут приходить пачками (coalescing).Coalescing (слияние событий) — это оптимизация, при которой несколько последовательных событий над одним и тем же файлом объединяются в одно. Например, множественные быстрые записи в файл могут породить только одно событие ENTRY_MODIFY.
Создание WatchService
import java.nio.file.*;
public class WatchServiceFactory {
public WatchService createWatchService() throws IOException {
// Получаем WatchService для файловой системы по умолчанию
WatchService watchService = FileSystems.getDefault().newWatchService();
return watchService;
}
}
FileSystems.getDefault().newWatchService() создаёт нативный дескриптор inotify (Linux), регистрирует callback в FSEvents (macOS) или создаёт объект ожидания ReadDirectoryChangesW (Windows). Этот дескриптор существует в нативной памяти вне кучи JVM.Регистрация директории
import java.nio.file.*;
public class DirectoryRegistrar {
public void registerDirectory(Path dir, WatchService watchService) throws IOException {
// Регистрируем директорию на отслеживание создания, удаления и модификации
WatchKey key = dir.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE, // создание файла/директории
StandardWatchEventKinds.ENTRY_DELETE, // удаление
StandardWatchEventKinds.ENTRY_MODIFY // изменение содержимого или метаданных
);
// WatchKey — это токен, идентифицирующий зарегистрированную директорию
// Его можно использовать для отмены регистрации
System.out.println("Директория зарегистрирована: " + dir);
}
}
WatchKey — это объект, представляющий регистрацию директории в WatchService. Он содержит ссылку на зарегистрированный путь и очередь событий. Когда WatchKey становится недействительным (например, директория удалена), его метод isValid() возвращает false.
#Java #для_новичков #beginner #IO #NIO #Files #Files_lines #WatchService
👍5
Ожидание событий: take() vs poll()
Важно: после обработки событий необходимо вызвать
Обработка WatchEvent
Важные нюансы WatchService
Событие CREATE приходит до завершения записи: если процесс создаёт файл и медленно пишет в него,
Событие MODIFY может приходить многократно: одна логическая операция записи может породить серию событий
Символические ссылки: WatchService следует по символическим ссылкам при регистрации. Если зарегистрировать ссылку, события будут приходить для целевой директории.
Рекурсивный мониторинг: WatchService не мониторит поддиректории автоматически. Для рекурсивного мониторинга нужно обходить дерево и регистрировать каждую директорию отдельно, а при появлении новой директории (событие
OVERFLOW: при слишком быстром потоке событий внутренний буфер ОС может переполниться. В этом случае приходит событие
Путь байтов в памяти JVM при построчном чтении
Files.lines(): путь данных
При
Метод создаёт
При итерации по
Чтение байтов из файла через
Декодирование байтов в символы через
Поиск символа перевода строки (
Создание нового объекта
Объект
После обработки строки в терминальной операции (например,
При закрытии
Путь байтов при WatchService
При создании
При регистрации директории (
При вызове
Объекты
При закрытии
Важно:
#Java #для_новичков #beginner #IO #NIO #Files #Files_lines #WatchService
import java.nio.file.*;
import java.util.concurrent.TimeUnit;
public class EventPoller {
// Блокирующее ожидание: поток ждёт, пока не появится событие
public void blockingWait(WatchService watchService) throws InterruptedException {
// take() блокирует поток до появления события
// Прерывается только при InterruptedException или закрытии WatchService
WatchKey key = watchService.take();
processKey(key);
}
// Неблокирующая проверка: возвращает null, если событий нет
public void nonBlockingPoll(WatchService watchService) {
WatchKey key = watchService.poll();
if (key != null) {
processKey(key);
} else {
System.out.println("Нет новых событий");
}
}
// Ожидание с таймаутом
public void timedPoll(WatchService watchService) throws InterruptedException {
// Ждём 5 секунд, если событий нет — возвращаем null
WatchKey key = watchService.poll(5, TimeUnit.SECONDS);
if (key != null) {
processKey(key);
}
}
private void processKey(WatchKey key) {
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path fileName = (Path) event.context(); // имя файла, вызвавшего событие
System.out.println("Событие: " + kind.name() + " для " + fileName);
}
// Сбрасываем ключ, чтобы получать новые события
boolean valid = key.reset();
if (!valid) {
System.out.println("Ключ больше не валиден — директория удалена");
}
}
}
take() — блокирующий метод, который переводит поток в состояние WAITING. Поток не потребляет CPU в этом состоянии. ОС пробуждает поток, когда приходит событие inotify/FSEvents/ReadDirectoryChangesW.poll() — неблокирующий. Если событий нет, немедленно возвращает null. Полезен в циклах, где нужно делать другую работу между проверками.poll(timeout, TimeUnit) — блокирует на указанное время, затем возвращает null или WatchKey.Важно: после обработки событий необходимо вызвать
key.reset(). Это сбрасывает состояние ключа и позволяет WatchService снова наполнять его событиями. Если reset() не вызван, ключ перестаёт получать новые события. Если reset() возвращает false, ключ больше не валиден (обычно потому, что директория удалена).Обработка WatchEvent
WatchEvent<?> event = ...;
// Тип события: ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY
WatchEvent.Kind<?> kind = event.kind();
// Имя файла или директории, вызвавшей событие (относительно зарегистрированной директории)
Path context = (Path) event.context();
// Количество событий данного типа для этого файла (может быть > 1 при coalescing)
int count = event.count();
event.context() возвращает Path, но это не абсолютный путь — это имя файла относительно зарегистрированной директории. Для получения абсолютного пути нужно резолвить через watchable() ключа:Path dir = (Path) key.watchable(); // зарегистрированная директория
Path absolute = dir.resolve(context); // полный путь
Важные нюансы WatchService
Событие CREATE приходит до завершения записи: если процесс создаёт файл и медленно пишет в него,
ENTRY_CREATE приходит сразу после появления inode. Файл может быть ещё пустым или заблокирован на запись. Решение — ожидание готовности (как в waitForFileReady()) или использование временных имён с последующим атомарным переименованием.Событие MODIFY может приходить многократно: одна логическая операция записи может породить серию событий
ENTRY_MODIFY. Приложение должно быть готово к дедупликации или игнорированию промежуточных событий.Символические ссылки: WatchService следует по символическим ссылкам при регистрации. Если зарегистрировать ссылку, события будут приходить для целевой директории.
Рекурсивный мониторинг: WatchService не мониторит поддиректории автоматически. Для рекурсивного мониторинга нужно обходить дерево и регистрировать каждую директорию отдельно, а при появлении новой директории (событие
ENTRY_CREATE для директории) регистрировать её динамически.OVERFLOW: при слишком быстром потоке событий внутренний буфер ОС может переполниться. В этом случае приходит событие
OVERFLOW, и часть событий теряется. Приложение должно обрабатывать это как сигнал к полному пересканированию директории.Путь байтов в памяти JVM при построчном чтении
Files.lines(): путь данных
При
Files.lines(file):Метод создаёт
BufferedReader — объект в куче, который оборачивает InputStreamReader, который оборачивает FileInputStream. Каждый из этих объектов — в куче. FileInputStream содержит файловый дескриптор (целое число в нативной памяти).BufferedReader выделяет буфер char[] размером 8192 символов (по умолчанию) в Young Generation. Этот буфер переиспользуется для всех операций чтения.При итерации по
Stream<String> каждый вызов readLine() выполняет:Чтение байтов из файла через
FileInputStream.read() в буфер BufferedReader.Декодирование байтов в символы через
CharsetDecoder (для UTF-8 — переменная длина, 1–4 байта на символ).Поиск символа перевода строки (
\n, \r\n или \r).Создание нового объекта
String для строки.Объект
String создаётся в Young Generation (Eden). Если строка длинная (например, CSV-строка с тысячей символов), она может быть размещена сразу в Old Generation как humongous object (в G1 GC, если размер превышает половину региона).После обработки строки в терминальной операции (например,
count() или forEach()) ссылка на String исчезает. Объект становится мусором и собирается Minor GC. Буфер BufferedReader остаётся живым до закрытия потока.При закрытии
Stream (через try-with-resources) вызывается BufferedReader.close(), который закрывает FileInputStream, который выполняет системный вызов close() для освобождения файлового дескриптора. Если поток не закрыт, дескриптор утечёт.Путь байтов при WatchService
При создании
WatchService:FileSystems.getDefault().newWatchService() создаёт объект WatchService в куче. На Linux это обёртка над InotifyWatchService, который выполняет системный вызов inotify_init().inotify_init() создаёт экземпляр inotify в ядре Linux и возвращает файловый дескриптор. Этот дескриптор хранится в нативной памяти JVM, ассоциированной с объектом WatchService.При регистрации директории (
dir.register(...)) выполняется inotify_add_watch(), который добавляет "watch" (наблюдение) за директорией в ядре. Ядро начинает отслеживать события для этой директории и всех её прямых потомков.inotify в ядре использует собственный буфер событий (обычно 16 КБ по умолчанию, настраивается через /proc/sys/fs/inotify/max_queued_events). Если событий больше, чем вмещает буфер, приходит OVERFLOW.При вызове
watchService.take() поток Java блокируется на системном вызове read() от дескриптора inotify. Поток переводится в состояние WAITING — он не потребляет CPU. Когда событие появляется, ядро пробуждает поток, read() возвращает структуру события, и JVM создаёт объекты WatchKey и WatchEvent в куче.Объекты
WatchKey и WatchEvent создаются в Young Generation. WatchKey может переживать несколько циклов событий, если его не сбросить через reset(). WatchEvent — одноразовый объект, который становится мусором после обработки.При закрытии
WatchService (watchService.close()) выполняется inotify_rm_watch() для всех зарегистрированных наблюдений и close() для дескриптора inotify. Это освобождает ресурсы ядра. Если WatchService не закрыт, дескриптор inotify остаётся открытым до завершения процесса.Важно:
inotify на Linux не рекурсивен. WatchService регистрирует наблюдение только для указанной директории, не для поддиректорий. Для рекурсивного мониторинга Java обходит дерево и создаёт отдельный inotify_add_watch() для каждой директории. Каждый watch потребляет ресурсы ядра (inode наблюдения), и их количество ограничено (/proc/sys/fs/inotify/max_user_watches, обычно 8192).#Java #для_новичков #beginner #IO #NIO #Files #Files_lines #WatchService
👍4