Java for Beginner
870 subscribers
1.01K photos
275 videos
14 files
1.69K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)

Глава 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()
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