W3docs

Потоки DataInput и DataOutput в Java

Чтение и запись примитивных типов Java в переносимом бинарном формате с помощью DataInputStream и DataOutputStream.

В этой части мы рассмотрели: байтовые потоки (обычные и буферизованные) для произвольных бинарных данных, символьные потоки для текста. Есть третий сценарий использования, который предыдущие главы не охватывают — запись int, double или boolean в файл и чтение их обратно в том же типе, в формате, с которым согласится другая JVM (работающая на другой ОС с другим порядком байт по умолчанию).

Именно для этого существуют DataInputStream и DataOutputStream. Это декораторы, которые располагаются поверх любого байтового потока и добавляют типизированные методы чтения/записи: writeInt, writeDouble, writeUTF, readInt, readDouble, readUTF. Бинарный формат задокументирован, фиксирован, использует порядок big-endian и переносим между всеми JVM.

Что записано — то и прочитано

DataOutputStream предоставляет по одному методу на каждый примитивный тип:

void writeBoolean(boolean v);    //  1 byte (0 or 1)
void writeByte(int v);            //  1 byte (low 8 bits)
void writeShort(int v);           //  2 bytes, big-endian
void writeChar(int v);            //  2 bytes, big-endian (UTF-16 code unit)
void writeInt(int v);             //  4 bytes, big-endian
void writeLong(long v);           //  8 bytes, big-endian
void writeFloat(float v);         //  4 bytes, IEEE 754
void writeDouble(double v);       //  8 bytes, IEEE 754
void writeUTF(String s);          //  modified UTF-8 with a 2-byte length prefix

DataInputStream имеет соответствующие методы readInt, readLong, readUTF и т. д. Контракт симметричен: записываете int с помощью writeInt, читаете его обратно через readInt — получаете то же число, всегда, на любой JVM, в любой операционной системе.

Три вещи, которые нужно усвоить:

  1. Формат не имеет разделителей полей. Файл с writeInt(42); writeUTF("alice"); writeDouble(3.14) — это 4 + 2 + 5 + 8 = 19 байт, записанных без каких-либо маркеров между ними. Читать нужно в том же порядке и с теми же типами. Нет схемы, нет самоописания, нет возможности восстановления при ошибке.

  2. writeUTF использует модифицированный UTF-8. Префикс — беззнаковая 16-битная длина (максимум 65 535 байт на строку), а U+0000 кодируется двумя байтами (0xC0 0x80) вместо стандартного одного байта. Формат несовместим с обычным UTF-8 — вы не можете прочитать строку, записанную writeUTF, с помощью Reader. Используйте его только когда обе стороны работают на Java.

  3. Big-endian, всегда. Порядок байт на машине варьируется (x86 — little-endian, сетевые протоколы — big-endian), но DataOutputStream всегда пишет в формате big-endian. Именно это делает формат переносимым. Если вам нужен little-endian для протокола, который вы не контролируете, используйте java.nio.ByteBuffer — у него есть настраиваемый порядок байт.

Когда стоит использовать потоки данных

Два случая:

  • Вы контролируете обе стороны и хотите простой, компактный, переносимый между языками бинарный формат. «Файл сохранения» для небольшой Java-игры, файл с тестовыми данными, кэш, который не должен переживать смену версии JVM. Формат легко записывать и разбирать; вам не нужна библиотека сериализации.
  • Вы читаете файловый формат, который случайно использует разметку Java-потоков данных. Class-файлы (.class), записи в формате RandomAccessFile, некоторые индексные файлы .jar. Все они были записаны с помощью DataOutputStream, потому что JDK сам создаёт этот формат.

Когда нужна межъязыковая совместимость (Python, Go, JS), используйте JSON, Protocol Buffers или MessagePack. Когда нужна версионность и эволюция схемы, ObjectOutputStream ближе к цели — но он тяжелее и имеет свои подводные камни.

Правило конца файла

Там где InputStream.read() возвращает -1 по достижении конца потока, DataInputStream.readInt() (и аналогичные методы) выбрасывает EOFException. Нет внутриполосного сигнала — корректное значение int может быть любым 32-битным значением, включая -1, поэтому единственный способ сигнализировать о конце потока — исключение.

try (DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)))) {
  try {
    while (true) {
      int x = in.readInt();
      process(x);
    }
  } catch (EOFException e) {
    // normal end of stream
  }
}

Этот try/catch для нормального завершения — идиоматическая конструкция. Необычно для JDK делать из исключения сигнал управления потоком, но у типизированного API чтения нет другого выхода — нет значения для возврата, которое не было бы одновременно допустимым int.

Для файлов, где вы контролируете формат, лучший паттерн — записать префикс длины в начало:

out.writeInt(n);
for (int i = 0; i < n; i++) out.writeInt(values[i]);

Тогда сторона чтения просто выполняет цикл n раз и никогда не ловит EOFException для управления потоком.

Буферизуйте перед декорированием

DataInputStream не буферизует. Каждый readInt превращается в серию вызовов read() на нижележащем потоке. Если этот поток — FileInputStream, каждый readInt — это четыре системных вызова. Всегда оборачивайте BufferedInputStream первым:

// Right
DataInputStream  in  = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)));
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(path)));

Это стандартный трёхуровневый стек: файл → буферизованный → данные. Тот же порядок применяется при записи. Пропустите буфер — и заплатите стоимостью системного вызова на байт из главы о буферизованных потоках, умноженной на количество байт на примитив.

Практический пример: простой бинарный формат записей

Программа ниже определяет минимальную бинарную запись — int id, UTF name, double score, boolean active — и записывает несколько записей во временный файл с помощью DataOutputStream. Читает их обратно через DataInputStream, используя паттерн с префиксом-счётчиком и паттерн с EOFException, и наконец демонстрирует сбой при несовпадении формата, когда читатель и писатель расходятся в типах полей.

java— editable, runs on the server

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

  • Размер файла оказался ровно таким, каким вы предсказали бы, сложив типизированные размеры: 4 (счётчик) + для каждой записи (4 + UTF с префиксом длины + 8 + 1). Без выравнивания, без разделителей. Файл потока данных — это байты как они есть, ничего лишнего.
  • Оба паттерна чтения дали одинаковые три записи. Паттерн с префиксом-счётчиком предпочтительнее при проектировании формата; паттерн с EOFException используется тогда, когда нельзя изменить писателя, а формат открытый.
  • Блок с несовпадением формата записал два int и прочитал один long. Байты на диске (00 00 00 2A 00 00 00 63) были корректны для обеих интерпретаций — DataInputStream не может их различить. Две интерпретации побайтово согласованы, но семантически неверны. Это цена бинарного формата без схемы: дисциплина на границе — единственная защита.
  • Каждый поток был обёрнут Files.newInputStreamBufferedInputStreamDataInputStream (и то же самое на стороне записи). Пропустите буфер — и readInt превратится в четыре системных вызова; слой потоков данных выполняет только преобразование формата и не добавляет никакой буферизации.
  • Для имени использовался writeUTF. Формат подходит для связи между Java-программами и бесполезен для всего остального — не стоит выбирать его для конфигурационного файла, который когда-нибудь придётся читать на Python. Для варианта «только Java, и я хочу компактность» — это правильный инструмент; для «кто-то ещё может это прочитать» — переходите к JSON или Protobuf.

Что дальше

Потоки данных обрабатывают по одному примитиву за раз и требуют, чтобы читатель знал формат. В следующей главе, Java PrintWriter, мы возвращаемся к символьной стороне и рассматриваем декоратор Writer, добавляющий методы print, println и printf — тот самый API, который вы используете с System.out с первой главы, наконец представленный как полноценный писатель в файл.

Практика

Практика
Файл был записан `DataOutputStream` на сервере Linux x86 (собственный порядок байт — little-endian) с помощью `out.writeInt(1)`. Что вернёт `DataInputStream.readInt()` на ноутбуке Windows ARM, читающем тот же файл?
Файл был записан `DataOutputStream` на сервере Linux x86 (собственный порядок байт — little-endian) с помощью `out.writeInt(1)`. Что вернёт `DataInputStream.readInt()` на ноутбуке Windows ARM, читающем тот же файл?
Was this page helpful?