I/O ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-31

Типы байтовых потоков в Java: InputStream и OutputStream

Классический баг при копировании бинарного файла: byte b; while ((b = (byte) in.read()) != -1). На текстовом файле всё работает, а картинка копируется обрезанной: первый же байт 0xFF после приведения к byte превращается в -1, и цикл решает, что файл закончился. Чтобы не ловить такие ошибки, нужно понимать, как устроены байтовые потоки.

Байтовые потоки в Java — это классы пакета java.io, которые читают и записывают данные по одному байту (8 бит) без какого-либо преобразования в символы. Все потоки чтения наследуются от абстрактного класса InputStream, все потоки записи — от OutputStream. Через них работают с любыми двоичными данными: изображениями, архивами, сетевыми пакетами, сериализованными объектами. Общее понятие потока данных и деление на байтовые и символьные потоки разобраны в уроке Потоки данных в Java.

1. Класс InputStream

InputStream — абстрактный класс, от которого наследуются все байтовые потоки чтения в Java. Он определяет методы read(), которые возвращают очередной байт как int от 0 до 255 или -1 в конце потока, а также методы для пропуска байтов, проверки доступных данных и закрытия источника.

Методы read() блокируют выполнение, пока не появятся данные, не будет достигнут конец потока или не будет выброшено исключение. При ошибке ввода-вывода методы выбрасывают IOException.

Методы класса InputStream

Метод Что делает Граничный случай
int read() Читает один байт и возвращает его как int от 0 до 255 В конце потока возвращает -1
int read(byte[] buffer) Читает до buffer.length байт в массив, возвращает число реально прочитанных байт Может прочитать меньше, чем размер массива; в конце потока — -1
int read(byte[] buffer, int offset, int length) Читает до length байт и кладёт их в массив, начиная с buffer[offset] IndexOutOfBoundsException, если offset/length выходят за границы массива
int available() Возвращает оценку количества байт, доступных для чтения без блокировки Это не размер файла: для сетевого потока часто возвращает 0
long skip(long n) Пропускает до n байт Возвращает количество реально пропущенных байт (может быть меньше n)
byte[] readAllBytes() (Java 9) Читает все оставшиеся байты до конца потока Для очень больших потоков возможен OutOfMemoryError
byte[] readNBytes(int len) (Java 11) Читает ровно len байт или меньше, если поток закончился раньше Не возвращает -1: в конце потока — пустой массив
long transferTo(OutputStream out) (Java 9) Копирует все оставшиеся байты в выходной поток Возвращает количество скопированных байт
void close() Закрывает источник и освобождает ресурсы ОС Последующие попытки чтения приведут к исключению IOException

Начиная с Java 11 есть фабричный метод InputStream.nullInputStream() — пустой поток, который сразу возвращает -1. Он удобен в тестах как заглушка.

2. Класс OutputStream

OutputStream — абстрактный класс, от которого наследуются все байтовые потоки записи. Он определяет методы write() для записи одного байта или массива байт, flush() для выталкивания буферизованных данных и close() для закрытия приёмника. При ошибке ввода-вывода методы выбрасывают IOException.

Методы класса OutputStream

Метод Что делает Граничный случай
void write(int data) Записывает один байт. Параметр имеет тип int, поэтому выражение не нужно приводить к byte Записываются только младшие 8 бит: write(256) запишет байт 0, write(-1) — байт 255
void write(byte[] buffer) Записывает весь массив байт NullPointerException, если массив равен null
void write(byte[] buffer, int offset, int length) Записывает length байт, начиная с buffer[offset] IndexOutOfBoundsException при выходе за границы массива
void flush() Выталкивает данные из буферов в приёмник У FileOutputStream ничего не делает; важен для BufferedOutputStream
void close() Сбрасывает буферы и закрывает поток Последующие попытки записи приведут к IOException

Аналогично InputStream, в Java 11 появился OutputStream.nullOutputStream() — поток, который молча отбрасывает все записанные байты.

3. FileInputStream и FileOutputStream

FileInputStream читает байты из файла, а FileOutputStream записывает байты в файл. Это основные байтовые потоки для работы с файлами: их используют для копирования и обработки любых файлов, а не только текстовых. Если файла для чтения нет, конструктор FileInputStream выбросит FileNotFoundException.

Конструкторы FileInputStream

InputStream in1 = new FileInputStream("D:/myprogram/java/test.txt");

File file = new File("D:/myprogram/java/test.txt");
InputStream in2 = new FileInputStream(file);

Первый конструктор принимает имя файла в качестве строки, второй — объект File (подробнее о нём — в уроке Класс File). Оба создают входной поток для чтения файла.

Конструкторы FileOutputStream

OutputStream out1 = new FileOutputStream("D:/myprogram/java/test.txt");

File file = new File("D:/myprogram/java/test.txt");
OutputStream out2 = new FileOutputStream(file);

// второй параметр true - дописывать в конец файла
OutputStream out3 = new FileOutputStream("D:/myprogram/java/log.txt", true);

Конструкторы создают выходной поток для записи в файл. Если файла нет, он будет создан; если не существует каталог, в котором он должен лежать, — будет выброшено FileNotFoundException.

Обратите внимание

Конструктор new FileOutputStream("log.txt") без второго параметра обнуляет существующий файл ещё до первой записи. Чтобы дописывать данные в конец, используйте new FileOutputStream("log.txt", true).

Пример 1. Копирование файла через FileInputStream и FileOutputStream

Потоки закрываются автоматически с помощью конструкции try-with-resources. Чтение идёт блоками по 8 КБ, а не по одному байту, — это в разы быстрее.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class FileCopy {
    public static void main(String[] args) {
        try (InputStream in = new FileInputStream("src/io/file.txt");
             OutputStream out = new FileOutputStream("src/io/copied_file.txt")) {

            byte[] buffer = new byte[8192];
            long total = 0;
            int count;
            while ((count = in.read(buffer)) != -1) {
                out.write(buffer, 0, count); // пишем только прочитанные байты
                total += count;
            }
            System.out.println("Скопировано байт: " + total);
        } catch (IOException e) {
            System.out.println("Ошибка: " + e.getMessage());
        }
    }
}

Вывод для файла размером 1 024 байта: Скопировано байт: 1024. Если файла src/io/file.txt нет, конструктор выбросит FileNotFoundException (наследник IOException), и программа выведет Ошибка: src/io/file.txt (...) с системным текстом причины.

Начиная с Java 9 весь цикл можно заменить одной строкой:

long total = in.transferTo(out);

Пример 2. Запись и чтение байтов

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class FileInputOutputStreamExample {
    public static void main(String[] args) {
        char[] symbols = {'a', 'b', 'c'};

        try (OutputStream output = new FileOutputStream("a.txt")) {
            for (char symbol : symbols) {
                output.write(symbol); // записываем код символа как один байт
            }
        } catch (IOException e) {
            System.out.println("Ошибка записи: " + e.getMessage());
        }

        try (InputStream input = new FileInputStream("a.txt")) {
            int data;
            while ((data = input.read()) != -1) { // читаем до конца файла
                System.out.print((char) data + " ");
            }
        } catch (IOException e) {
            System.out.println("Ошибка чтения: " + e.getMessage());
        }
    }
}
// Вывод: a b c

Приведение (char) data корректно только для ASCII-символов: каждый из них занимает ровно один байт. Кириллица в UTF-8 занимает 2 байта, и побайтовое чтение превратит её в «кракозябры». Для текста используйте символьные потоки — см. урок Классы символьных потоков.

4. ByteArrayInputStream и ByteArrayOutputStream

ByteArrayInputStream читает байты из массива в памяти, а ByteArrayOutputStream записывает байты в автоматически растущий массив, который можно получить методом toByteArray(). Эти классы нужны, когда метод принимает InputStream/OutputStream, а данные лежат в памяти, — например, в юнит-тестах.

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class ByteArrayStreamsExample {
    public static void main(String[] args) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write(72);                                          // 'H'
        out.writeBytes("i!".getBytes(StandardCharsets.UTF_8)); // Java 11

        byte[] bytes = out.toByteArray();
        System.out.println(Arrays.toString(bytes));
        System.out.println(out.toString(StandardCharsets.UTF_8)); // Java 10

        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        int first = in.read();
        System.out.println((char) first + ", осталось байт: " + in.available());
    }
}
// Вывод:
// [72, 105, 33]
// Hi!
// H, осталось байт: 2

Здесь не нужен ни try-catch, ни throws IOException: у классов ByteArray* методы read(), write(int) и writeBytes() переопределены без объявления IOException — в памяти ошибке ввода-вывода взяться неоткуда. По той же причине available() у ByteArrayInputStream возвращает точное число оставшихся байт.

5. BufferedInputStream и BufferedOutputStream

BufferedInputStream и BufferedOutputStream — обёртки (декораторы) над другим байтовым потоком, которые читают и записывают данные блоками через внутренний буфер размером 8 КБ по умолчанию. Они ускоряют побайтовую обработку: вместо системного вызова на каждый read() поток обращается к файлу один раз на блок.

try (InputStream in = new BufferedInputStream(new FileInputStream("photo.jpg"));
     OutputStream out = new BufferedOutputStream(new FileOutputStream("photo-copy.jpg"))) {
    int b;
    while ((b = in.read()) != -1) { // read() берёт байт из буфера в памяти
        out.write(b);
    }
} // close() сам вызовет flush() и допишет остаток буфера

Размер буфера можно задать вторым параметром конструктора: new BufferedInputStream(in, 64 * 1024). Если вы и так читаете большими массивами, как в примере 1, буферизованная обёртка почти ничего не добавит.

Важно

Если не закрыть BufferedOutputStream и не вызвать flush(), последние байты (до 8 КБ) останутся в буфере и не попадут в файл. Закрывать достаточно внешнюю обёртку — она закроет и вложенный FileOutputStream.

6. На чём чаще всего ошибаются

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

Что выведет этот код?

byte[] data = {10, (byte) 0xFF, 20};
InputStream in = new ByteArrayInputStream(data);

int count = 0;
byte b;
while ((b = (byte) in.read()) != -1) {
    count++;
}
System.out.println(count);

Ответ: 1, а не 3. Метод read() вернул для второго байта число 255, но после приведения (byte) 255 стало -1, и цикл завершился, хотя поток ещё не закончился. Именно поэтому read() возвращает int: значения 0–255 — это данные, а -1 — сигнал конца потока, и их нельзя перепутать. Исправление — хранить результат в int и приводить к byte только после проверки:

int value;
while ((value = in.read()) != -1) {
    count++;
}
// Вывод: 3

Другие типичные промахи

  • Цикл по available(). Код for (int i = 0; i < in.available(); i++) работает с локальным файлом, но для сетевого потока available() часто возвращает 0, и ничего не будет прочитано. Читайте до -1.
  • out.write(buffer) вместо out.write(buffer, 0, count). Последний блок обычно заполнен не полностью, и в копию попадут «хвосты» предыдущего блока — файл станет больше оригинала.
  • Закрытие в finally без проверки на null или вообще без закрытия. Используйте try-with-resources: поток закроется даже при исключении.

7. Таблица классов байтовых потоков

В пакете java.io около двадцати байтовых потоков. Большинство из них — либо источники и приёмники данных (файл, массив, канал), либо декораторы-наследники FilterInputStream/FilterOutputStream, которые добавляют возможности к другому потоку: буферизацию, чтение примитивов, возврат байта обратно.

Класс Наследуется от Назначение
InputStream Object Абстрактный класс, описывающий байтовый поток ввода
OutputStream Object Абстрактный класс, описывающий байтовый поток вывода
FileInputStream InputStream Входной поток, читающий из файла
FileOutputStream OutputStream Выходной поток, записывающий в файл
ByteArrayInputStream InputStream Входной поток, читающий из массива байт
ByteArrayOutputStream OutputStream Выходной поток, записывающий в массив байт
FilterInputStream InputStream Базовый класс для декораторов входных потоков
FilterOutputStream OutputStream Базовый класс для декораторов выходных потоков
BufferedInputStream FilterInputStream Буферизованный входной поток
BufferedOutputStream FilterOutputStream Буферизованный выходной поток
DataInputStream FilterInputStream Входной поток с методами для чтения примитивных типов Java (readInt(), readDouble(), readUTF())
DataOutputStream FilterOutputStream Выходной поток с методами для записи примитивных типов Java
PushbackInputStream FilterInputStream Входной поток, позволяющий вернуть прочитанный байт обратно (операция pushback)
PrintStream FilterOutputStream Выходной поток с методами print(), println(), printf(); к этому типу относится System.out
ObjectInputStream InputStream Входной поток для чтения сериализованных объектов
ObjectOutputStream OutputStream Выходной поток для сериализации объектов
PipedInputStream InputStream Входной канал для передачи данных между потоками выполнения
PipedOutputStream OutputStream Выходной канал, связанный с PipedInputStream
SequenceInputStream InputStream Входной поток, который последовательно читает два и более входных потока, один за другим
RandomAccessFile Object Не является потоком: позволяет перемещаться по файлу (seek()), читать из него и писать в него

Мост между байтовыми и символьными потоками — классы InputStreamReader и OutputStreamWriter; они описаны в уроке Классы символьных потоков.

Часто задаваемые вопросы

Как прочитать весь InputStream в массив байт или строку?

С Java 9 — методом in.readAllBytes(). Строку из байтов получают так: new String(in.readAllBytes(), StandardCharsets.UTF_8). Кодировку указывайте явно, иначе будет использована кодировка по умолчанию. Для гигабайтных файлов этот способ не подходит — читайте блоками или используйте transferTo().

Почему InputStream.read() возвращает int, а не byte?

Типу byte не хватает значений: все 256 его вариантов могут быть данными, и для признака конца потока места не остаётся. Поэтому read() возвращает данные как 0–255, а конец потока — как -1. Разбор ошибки с примером — в разделе На чём чаще всего ошибаются.

Есть ли в Java CountingInputStream и CountingOutputStream?

В стандартной библиотеке JDK таких классов нет. Готовые реализации есть в Apache Commons IO (org.apache.commons.io.input.CountingInputStream, org.apache.commons.io.output.CountingOutputStream) и в Guava (com.google.common.io.CountingInputStream/CountingOutputStream). Свой счётчик байт легко написать, унаследовавшись от FilterInputStream и переопределив методы read().

Нужно ли закрывать ByteArrayOutputStream?

Технически нет: у ByteArrayInputStream и ByteArrayOutputStream метод close() ничего не делает, а после него методы продолжают работать без IOException. Закрывать обязательно потоки, связанные с ресурсами ОС: файлами, сокетами, а также обёртки над ними.

Practice

Reinforce this lesson with hands-on tasks, each with a level and a time estimate.

All module tasks →

Comments

Please log in or register to have a possibility to add comment.