W3docs

Байтовые потоки в Java

Чтение и запись двоичных данных в Java с помощью InputStream, OutputStream, FileInputStream и FileOutputStream.

В главе 1 архитектура java.io была описана как стек декораторов: сырой поток в основании, слои функциональности поверх него, а верхний слой предоставляет API, которым вы пользуетесь. Первые шесть глав этой части работали на вершине стека — Files.readString, Files.lines, Files.writeString. Эта глава опускается на уровень ниже — к байт-ориентированной абстракции, на которой построен весь стек: InputStream и OutputStream.

Каждый файл, сокет, канал и буфер в памяти в java.io — это в самом низу поток байт. Даже текстовый файл в формате UTF-8 — это байты на диске; «текстовое» представление появляется благодаря Reader, надстроенному поверх InputStream. Знание байтового API необходимо тогда, когда данные не являются текстом (изображения, аудио, архивы, сетевые протоколы), когда нужно копировать байты без их декодирования, и когда вы хотите понять, что делают высокоуровневые API на самом деле.

Контракт InputStream

InputStream — абстрактный класс с одним методом:

public abstract int read() throws IOException;

Он возвращает следующий байт как int в диапазоне 0..255 или -1 при исчерпании потока. Тип int — не ошибка: byte в Java знаковый (-128..127), но контракт потока подразумевает беззнаковое значение, поэтому расширенный возвращаемый тип позволяет отличить «конец потока» (-1) от реального значения байта (0xFF возвращается как 255, а не как -1).

На основе read() определены ещё три метода, которые вы обычно и вызываете:

int read(byte[] buf);                  // read up to buf.length bytes; return count or -1
int read(byte[] buf, int off, int len); // same, into a slice
byte[] readAllBytes();                  // Java 9+: read everything into a byte[]
long transferTo(OutputStream out);       // Java 9+: pipe straight to a sink, no copy loop

readAllBytes() — удобный метод для небольших файлов; transferTo — для копирования без декодирования. Для всего остального используется цикл буферизованного чтения, который имеет каноническую форму:

byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
  out.write(buf, 0, n);                 // n bytes, not buf.length — the last chunk is short
}

Две вещи, которые нужно запомнить. Во-первых, вызовы read(byte[]) возвращают фактическое количество прочитанных байт, а не всегда buf.length. Последнее чтение почти всегда неполное; обращение с буфером как с полным искажает данные. Во-вторых, read() и read(byte[]) являются блокирующими — они возвращают управление, когда доступен хотя бы один байт или поток завершён. Они не возвращают управление досрочно на медленном диске или медленном сокете.

Пропуск, просмотр вперёд и перемотка

InputStream также определяет три метода, к которым обращаются реже, но которые стоит знать:

long skip(long n);     // discard up to n bytes without copying them anywhere
int  available();      // bytes you can read right now without blocking — an estimate, not a length
boolean markSupported();
void mark(int readAheadLimit);  // remember this position
void reset();                    // jump back to the last mark

Здесь есть две ловушки. available() — это не размер потока: для файла он часто совпадает, но для сокета это «уже буферизованные байты», которых может быть 0 во время передачи. Никогда не пишите new byte[in.available()], рассчитывая прочитать всё целиком. А mark/reset работают, только если markSupported() возвращает true; сырой FileInputStream возвращает false, поэтому оберните его в BufferedInputStream (следующая глава), если нужно заглядывать вперёд и возвращаться назад.

Контракт OutputStream

Зеркальный класс — OutputStream, тоже с одним абстрактным методом:

public abstract void write(int b) throws IOException;

Он записывает младшие 8 бит b и игнорирует остальные. Удобные перегрузки:

void write(byte[] buf);                    // write the whole array
void write(byte[] buf, int off, int len);  // write a slice — this is the one you usually want
void flush();                               // push buffered data to the OS
void close();                               // flush + release resources

flush() имеет значение только если поток буферизует данные. Сырой FileOutputStream не буферизует — каждый write обращается к ОС — поэтому flush является заглушкой. Буферизация и необходимость в flush появляются в BufferedOutputStream (следующая глава).

close() вызывает flush() первым. Вот почему «забытое закрытие буферизованного потока» молча обрезает файл: хвостовой буфер находится в памяти в ожидании flush, который так и не происходит.

Конкретные байтовые потоки

Конкретные подклассы, которые вы будете инстанциировать напрямую:

КлассЧто оборачивает
FileInputStream / FileOutputStreamФайл на диске. Открывает файловый дескриптор.
ByteArrayInputStream / ByteArrayOutputStreambyte[] в памяти. Полезно для тестов и захвата вывода.
BufferedInputStream / BufferedOutputStreamБуферизованное представление другого потока.
PipedInputStream / PipedOutputStreamКанал производитель/потребитель между потоками.
DataInputStream / DataOutputStreamНадстройка над байтовым потоком для переносимого чтения/записи примитивов.

FileInputStream и FileOutputStream — сырые файловые потоки. Они небуферизованы: каждый вызов read()/write() — это один системный вызов. Это катастрофически плохо для циклов побайтового чтения (миллионы системных вызовов), но вполне приемлемо для чтения блоками размером 8 КБ и более. Буферизованная глава — это то, что делает побайтовый API приемлемым.

// Raw, unbuffered — fine for chunked reads
try (FileInputStream in = new FileInputStream("photo.jpg")) {
  byte[] buf = new byte[8192];
  int n;
  while ((n = in.read(buf)) != -1) { /* process buf[0..n] */ }
}

// Equivalent one-liner, Java 7+
byte[] all = Files.readAllBytes(Path.of("photo.jpg"));

Files.readAllBytes — правильный выбор для небольших файлов; для всего, что может не поместиться в памяти, безопасна форма с блочным циклом.

Три шаблона, которые стоит запомнить

Три вещи, которые вы делаете с байтовыми потоками снова и снова:

// 1. Copy a file
try (InputStream in  = Files.newInputStream(src);
     OutputStream out = Files.newOutputStream(dst)) {
  in.transferTo(out);                                 // Java 9+: no manual loop
}
// Java 7+ one-liner: Files.copy(src, dst);

// 2. Read everything into memory
byte[] all = Files.readAllBytes(path);                 // small-file shortcut

// 3. Build a byte[] you don't know the size of in advance
ByteArrayOutputStream baos = new ByteArrayOutputStream();
in.transferTo(baos);
byte[] bytes = baos.toByteArray();

ByteArrayOutputStream — это «растущий по мере необходимости» байтовый приёмник. Именно так JDK реализует readAllBytes() для потоков, длина которых заранее неизвестна. Он никогда не генерирует исключение при write (пока не кончится куча) и не имеет значимой семантики close(), что делает его стандартным тестовым инструментом для «захвата того, что записал этот writer».

Когда использовать байтовые потоки

Честный ответ: когда данные не являются текстом. Всё двоичное — изображения, аудио, видео, архивы (.zip, .tar), исполняемые файлы, protocol buffers, пользовательские форматы файлов — это байты, и они остаются байтами.

Когда данные являются текстом, предпочтительнее символьная сторона потоков (Reader/Writer, следующая глава) или современные Files.readString / Files.lines. Чтение текстового файла как сырых байт и ручное декодирование — это стандартный способ создать собственный баг с кодировкой: многобайтовые символы UTF-8 могут быть разделены между вызовами read(), и вы неправильно их соберёте. Слой Reader существует именно для того, чтобы вам не пришлось об этом думать.

Практический пример: копирование, хеширование и захват

Программа ниже демонстрирует байтовый API потоков от начала до конца. Она записывает небольшой двоичный файл (заголовок и полезная нагрузка), читает его обратно по блокам для вычисления контрольной суммы, копирует в другой файл с помощью transferTo и захватывает ещё одну копию в ByteArrayOutputStream, чтобы продемонстрировать приёмник в памяти в действии. Временные файлы удаляются при выходе.

java— editable, runs on the server

Что стоит вынести из этого запуска:

  • Сторона записи использовала Files.newOutputStream — фабричный метод в стиле Files, возвращающий обычный OutputStream. Как только он получен, API — тот самый, что был в Java с версии 1.0. Фабрика просто избавляет от необходимости конструировать FileOutputStream и думать об опциях открытия.
  • В цикле чтения использовалось n, а не buf.length, при вызове crc.update. Причина видна в строке вывода: «read in N chunks». Буфер был размером 256 байт, а файл — 1004 байта, поэтому последний блок оказался неполным. Использование buf.length хешировало бы мусор за пределами реальных данных.
  • in.transferTo(out) — это проверенный JDK-цикл копирования. Он заметно быстрее написанного вручную цикла на большинстве JVM, поскольку может использовать буфер на 16 КБ и пропускать точки безопасности, и при этом занимает одну строку вместо пяти. Используйте его всякий раз, когда иначе написали бы цикл while ((n = in.read(buf)) != -1) без другой логики внутри.
  • ByteArrayOutputStream напрямую подключился к transferTo. Он выглядит как файл, но живёт в памяти — тот же API. Эта симметрия делает java.io тестируемым: передайте ByteArrayInputStream в качестве источника и ByteArrayOutputStream в качестве приёмника — и можно юнит-тестировать код, «записывающий в файл», не касаясь диска.
  • Последний блок вывел 255, затем -1. Это и есть контракт: 0xFF — допустимое значение байта, которое возвращается как 255; -1 — это внешнеполосный сигнал о том, что «байт больше нет». Если хранить результат в byte (вместо int) и сравнивать с == -1, реальный 0xFF будет молча восприниматься как конец потока. Всегда сохраняйте результат в int и сравнивайте с -1 перед приведением типа.

Что дальше

Байты — правильная абстракция для двоичных данных. В следующей главе, Java Character Streams, рассматривается параллельная иерархия для текста — Reader и Writer, сопряжение кодировок и почему «просто new FileReader(path)» — классический источник ошибок «работает у меня, сломано на сервере».

Практика

Практика
Что возвращает `InputStream.read()`, когда поток содержит один байт со значением `0xFF`, и что он возвращает при следующем вызове?
Что возвращает `InputStream.read()`, когда поток содержит один байт со значением `0xFF`, и что он возвращает при следующем вызове?
Практика
В цикле `while ((n = in.read(buf)) != -1) out.write(buf, 0, n);` почему передаётся `n`, а не `buf.length` в `write`?
В цикле `while ((n = in.read(buf)) != -1) out.write(buf, 0, n);` почему передаётся `n`, а не `buf.length` в `write`?
Was this page helpful?