W3docs

Обзор Java NIO

Введение в Java NIO и NIO.2 — каналы, буферы, селекторы и пакет java.nio.file.

Пятнадцать предыдущих глав были посвящены java.io — потокам, Reader/Writer, File, Serializable. Это оригинальный I/O-API Java, который по сей день широко используется. NIO — семейство API, добавленных в Java позже, чтобы покрыть то, чего не хватало в java.io. Оно состоит из двух частей, объединённых общим префиксом пакета, но мало чем ещё:

  • NIO (Java 1.4, 2002) — java.nio.* — каналы, буферы, селекторы. Другой подход к I/O: ориентированный на байтовые буферы, опционально неблокирующий, разработанный для высокопроизводительных серверов.
  • NIO.2 (Java 7, 2011) — java.nio.file.* — классы Path, Files, FileSystem и WatchService. Более удобная замена java.io.File и место для возможностей файловой системы, которых java.io никогда не имел: символические ссылки, расширенные атрибуты, асинхронный файловый I/O, слежение за директориями.

Вы уже используете элементы NIO.2 с самого начала этой части: Path, Files.newBufferedReader, Files.newInputStream — всё это java.nio.file. Эта глава делает шаг назад и показывает, как все эти части соотносятся друг с другом, и для чего предназначена остальная часть пакета.

Поток против канала: две разных формы

InputStream.read() возвращает один байт. OutputStream.write(int) записывает один байт. Концептуальная модель — поэтапная передача по одному байту. Буферизующие декораторы ускоряют её, но абстракция остаётся последовательной и однонаправленной.

Канал (java.nio.channels.Channel) — двунаправленный, ориентированный на байтовые буферы, и поддерживает операции, которые InputStream не умеет выражать:

  • Чтение в ByteBuffer и запись из него — не из byte[].
  • Отображение в память области файла в RAM и чтение/запись её как буфера.
  • Scatter-чтение в несколько буферов (заголовок — в один, полезная нагрузка — в другой).
  • Gather-запись из нескольких буферов (один write() создаёт непрерывный вывод).
  • Пометить канал неблокирующим и позволить Selector мультиплексировать тысячи каналов в одном потоке.

Плата — многословность. Код с каналами читает и пишет через ByteBuffer с явными вызовами flip() и position(); java.io скрывает всё это за read(byte[]). Для типичного чтения файлов предпочтительнее API java.io/Files. Опускайтесь до каналов только тогда, когда вам нужна одна из их эксклюзивных возможностей.

// channel-shaped read into a 1 KB buffer
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
  ByteBuffer buf = ByteBuffer.allocate(1024);
  int n = ch.read(buf);                              // fills the buffer; updates position
  buf.flip();                                        // switch to "read what was just written"
  while (buf.hasRemaining()) {
    process(buf.get());
  }
}

Шаг flip() — это тот момент, когда люди узнают, что у ByteBuffer есть собственный небольшой конечный автомат.

ByteBuffer: position, limit, capacity

ByteBuffer — это byte[] фиксированного размера (или фрагмент внеградуированной памяти) плюс три индекса:

  • position — следующий байт для чтения или записи.
  • limit — индекс за последним байтом, который разрешено трогать.
  • capacity — фиксированный размер буфера; не меняется.
0 ─────── position ─────── limit ─────── capacity
   (consumed)   (active region)   (untouchable / empty)

По соглашению буфер находится в одном из двух режимов:

  • Режим записи (по умолчанию): вы помещаете в него байты через put(byte). position продвигается; limit == capacity.
  • Режим чтения: вы извлекаете байты через get(). position продвигается; limit — там, где вы остановили запись.

flip() переключает из режима записи в режим чтения: устанавливает limit = position (отмечает конец данных) и сбрасывает position = 0 (начинает чтение с начала). clear() переключает обратно в режим записи (position = 0, limit = capacity). Ошибки здесь — самый частый источник фрустрации «я прочитал ноль байт; почему?».

Внеградуированные буферы (ByteBuffer.allocateDirect(n)) обходят кучу JVM и позволяют ОС читать/писать их напрямую без лишнего копирования. Они медленнее при выделении, быстрее при I/O, и являются правильным выбором только для кода на горячем пути I/O.

Селекторы: один поток, много каналов

До виртуальных потоков (Java 21), обслуживание тысяч одновременных сетевых соединений в Java означало либо тысячи потоков ОС (по одному на соединение — дорого), либо один поток, мультиплексирующий с помощью Selector:

Selector sel = Selector.open();
serverChannel.register(sel, SelectionKey.OP_ACCEPT);
while (true) {
  sel.select();                                       // blocks until any channel is ready
  for (SelectionKey k : sel.selectedKeys()) {
    if (k.isAcceptable()) accept(k);
    if (k.isReadable())   read(k);
  }
}

ОС уведомляет JVM, когда любой зарегистрированный канал может продвинуться; JVM передаёт вам готовый набор; вы выполняете неблокирующее чтение или запись и возвращаетесь к select(). Фреймворковый код Netty, gRPC и Spring WebFlux имеет именно такую форму.

С виртуальными потоками (Thread.startVirtualThread(...)), более простой паттерн «один поток на запрос» масштабируется до такого же количества соединений без хореографии Selector — виртуальные потоки приостанавливаются на блокирующем I/O практически бесплатно. Для нового кода приложений на Java 21+, цикл селектора всё чаще становится заботой библиотеки; вы обычно не пишете его вручную. Для библиотечного кода и JVM до Loom это стандартный паттерн.

java.nio.file: современный API для работы с файлами

Это та половина NIO, с которой вы будете работать в повседневном коде. Она заменяет java.io.File и большинство связанных с файлами частей java.io:

java.iojava.nio.fileПричина замены
FilePathНеизменяемый, OS-агностичный, без встроенных методов I/O
File.list()Files.list(Path), Files.walk(Path)Stream<Path>; закрываемый; учитывает символические ссылки
new FileInputStream(...)Files.newInputStream(path)Варианты с учётом кодировки для текста; единый последовательный API открытия
file.delete(), возвращающий false при сбоеFiles.delete(path), бросающий IOExceptionСбои видимы, а не молчаливы
нет эквивалентаFiles.walkFileTree, WatchService, API символических ссылок, представления атрибутов файловВозможности, которых java.io никогда не имел

Две следующие главы подробно рассматривают Path и Files. Практическое правило: для работы с файлами в Java 2024+ используйте java.nio.file. java.io.File всё ещё существует, потому что его использует старый код, но новый код должен по умолчанию выбирать Path.

Практический пример: круговой обход файла через канал и буфер

Программа ниже копирует небольшой текстовый файл с помощью каналов и буферов, чтобы сделать position/limit/flip конкретными. Она открывает источник как FileChannel, читает в ByteBuffer, вызывает flip, пишет в FileChannel-назначение и выводит состояние буфера на каждом шаге, чтобы вы могли видеть, как двигаются индексы.

java— editable, runs on the server

Что можно вынести из запуска:

  • Цикл печатал состояние буфера на каждом шаге. После read(), position был равен числу прочитанных байт, а limit по-прежнему равен capacity — это «режим записи»: место в конце есть. После flip(), position = 0, а limit = только что прочитанному числу — это «режим чтения»: байты находятся между 0 и limit. Два индекса кодируют «где живут данные» без их копирования.
  • Буфер был 16 байт; файл — 44. Цикл выполнился три итерации: 16, 16, 12. Как только буфер опустел (после того как write слил его), clear() сбросил его обратно в «режим записи», чтобы следующий read() мог заново заполнить его. Это паттерн каналов в миниатюре: заполнить, перевернуть, слить, очистить, повторить.
  • transferTo выполнил то же копирование в одну строку без какого-либо ByteBuffer. На Linux это отображается в единственный системный вызов sendfile() — байты перемещаются между ядрами без пересечения JVM. Когда вы перемещаете данные между двумя каналами и не нужно их смотреть, это правильный инструмент.
  • Обратите внимание, что исходный файл был создан через Files.writeString, а назначение прочитано обратно через Files.readString — оба являются однострочниками java.nio.file, которые полностью скрывают каналы и буферы. Подробный цикл с каналами посередине — это то, что вы пишете только тогда, когда вам нужен прямой доступ к буферу (пользовательский бинарный разбор, отображение памяти, scatter/gather). Для «копирования файла» transferTo или Files.copy короче и по меньшей мере так же быстро.
  • Конструктор FileChannel.open(path, OPTION) является аналогом Files.newInputStream(path). Перечисление StandardOpenOption (READ, WRITE, CREATE, APPEND, TRUNCATE_EXISTING, ...) управляет поведением открытия — есть только одно место для поиска. Это перечисление параметров открытия постоянно встречается в следующей главе.

Что дальше

Эта глава перечислила элементы — каналы, буферы, селекторы, java.nio.file. Следующая глава, Класс Path в Java, подробно рассматривает наиболее удобный из этих элементов — Path — и методы (resolve, relativize, normalize), которые вы будете использовать каждый раз при работе с путями файловой системы.

Практика

Практика
Вы прочитали 10 байт из канала в `ByteBuffer` ёмкостью 1024. Вы хотите записать эти 10 байт в другой канал. Что нужно сделать между `read()` и `write()`?
Вы прочитали 10 байт из канала в `ByteBuffer` ёмкостью 1024. Вы хотите записать эти 10 байт в другой канал. Что нужно сделать между `read()` и `write()`?
Was this page helpful?