Типы байтовых потоков в 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