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

Потоки данных (stream) в Java: ввод-вывод и упаковка потоков

Программа записала отчёт через BufferedWriter, завершилась без единого исключения — а файл report.txt пустой, 0 байт:

BufferedWriter writer = new BufferedWriter(new FileWriter("report.txt"));
writer.write("Итого: 42");
// close() не вызван, программа завершилась
// Результат: report.txt создан, но его размер 0 байт

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

Поток данных (stream) в Java — это упорядоченная последовательность байтов или символов, которая передаётся между программой и источником (source) или приёмником (destination) данных: файлом, консолью, сетевым соединением, массивом в памяти. Потоки бывают входные (читают данные) и выходные (записывают данные), байтовые и символьные; их классы находятся в пакете java.io.

Что такое поток данных в Java

Поток данных в Java — это абстракция ввода-вывода: программа читает или записывает данные по очереди, элемент за элементом, не зная, какое физическое устройство стоит на другом конце. Поток ввода связан с источником данных, поток вывода — с приёмником.

Поток данных в Java: источник, программа и приёмник

В информатике потоком вообще называют последовательность данных, которая становится доступна постепенно, а не целиком сразу. В Java потоки ввода-вывода реализованы иерархией классов, определённых в пакете java.io. Эти классы избавляют от необходимости разбираться в низкоуровневом устройстве операционной системы: программа работает с файлом, клавиатурой или сетью через одни и те же методы read() и write().

Все потоки ввода-вывода ведут себя одинаково, независимо от конкретного устройства. Один и тот же код, читающий из InputStream, сможет читать из файла на диске, из сетевого сокета или из стандартного ввода System.in (о нём — в уроке Предопределенные потоки ввода-вывода). Поток читается последовательно: прочитанный байт «уходит», и вернуться к нему без специальных классов нельзя.

I/O-потоки, Stream API и Thread: три разных «потока»

В русскоязычной литературе словом «поток» называют три разные вещи в Java: поток ввода-вывода (java.io.InputStream и др.), поток Stream API (java.util.stream.Stream, Java 8) и поток выполнения (java.lang.Thread, см. многопоточность в Java). Этот урок — только о первом: о передаче байтов и символов.

«Поток» Классы Что это Пример
Поток ввода-вывода (I/O stream) InputStream, OutputStream, Reader, Writer (java.io) Последовательность байтов или символов между программой и файлом, консолью, сетью new FileInputStream("photo.jpg")
Stream API Stream<T>, IntStream (java.util.stream, с Java 8) Конвейер операций над элементами коллекции или массива; ничего не читает с диска сам по себе list.stream().filter(s -> s.isEmpty()).count()
Поток выполнения (thread) Thread, Runnable (java.lang) Отдельная ветка выполнения кода, работающая параллельно с другими new Thread(task).start()

Названия пересекаются, но связи между ними нет: FileInputStream не наследует Stream и не запускает отдельный Thread. Единственная точка соприкосновения — метод BufferedReader.lines(), который превращает строки из I/O-потока в Stream<String>.

Виды потоков: байтовые и символьные, ввода и вывода

Потоки в Java делятся по двум признакам: по типу данных — на байтовые и символьные, по направлению — на потоки ввода (input) и вывода (output). Сочетание признаков даёт четыре основных абстрактных класса: InputStream, OutputStream, Reader и Writer.

По типу данных в Java существует 2 вида потоков:

  • Байтовые потоки (binary streams) передают байты — восьмибитные значения. Подходят для любых данных: изображений, архивов, аудио, сериализованных объектов.

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

  • Символьные потоки (text streams) передают символы char — 16-битные единицы Unicode (UTF-16). Подходят для текста.

Символьный поток в Java

Важно понимать: в файле или в сети всегда лежат байты. Символьный поток — это байтовый поток плюс кодировка, которая переводит байты в символы и обратно (подробнее — в разделе Цепочка обёрток).

По направлению потоки делятся на потоки ввода (input) и потоки вывода (output). В Java определены четыре основных абстрактных класса для работы с потоками:

Основные абстрактные классы потоков данных: InputStream, OutputStream, Reader, Writer

Направление Байтовые потоки Символьные потоки Пример наследника
Ввод (чтение) InputStream Reader FileInputStream, FileReader
Вывод (запись) OutputStream Writer FileOutputStream, FileWriter

По имени класса легко определить его вид: суффикс ...Stream — байтовый, ...Reader/...Writer — символьный. Методы и конкретные классы разобраны в уроках Типы байтовых потоков в Java и Классы символьных потоков.

Общая схема работы с потоками

Работа с любым потоком ввода-вывода в Java сводится к трём шагам: открыть поток, связанный с источником или приёмником, читать или писать данные, пока они есть, и закрыть поток, чтобы освободить ресурс операционной системы и сбросить буферы.

  1. Создать потоковый объект и связать его с источником или приёмником данных (файлом на диске, сокетом, массивом).
  2. Пока есть информация, читать данные из потока или записывать данные в поток.
  3. Закрыть поток.

В современном коде третий шаг выполняет конструкция try-with-resources (Java 7+): поток закрывается автоматически, даже если внутри возникло исключение. Скелет любой работы с потоком выглядит так:

try (InputStream in = new FileInputStream("data.bin")) { // 1. открыть
    int b;
    while ((b = in.read()) != -1) {                       // 2. читать, пока не -1
        // обработать байт b
    }
}                                                         // 3. close() вызовется сам

Подробно конструкция разобрана в уроке Конструкция try-with-resources.

Упаковка (wrapping) потоков: паттерн «декоратор»

Упаковка (wrapping) потоков — это приём, при котором один поток передаётся в конструктор другого и получает от него новые возможности: буферизацию, чтение строк, форматированный вывод, перевод байтов в символы. Внешний поток называют обёрткой; он делегирует реальное чтение и запись внутреннему потоку.

Иначе говоря, потокам можно придать новые свойства, заключив один поток в оболочку другого. По-другому это называется упаковкой (wrapping) классов. В основе лежит паттерн проектирования «декоратор»: обёртка имеет тот же тип, что и оборачиваемый поток (BufferedReader — тоже Reader), поэтому обёртки можно вкладывать друг в друга в любом нужном порядке, не создавая отдельный класс под каждую комбинацию.

Класс BufferedReader применяют для более эффективного чтения символов, массивов и строк:

BufferedReader reader = new BufferedReader(new FileReader("input.txt"));

Классы BufferedWriter и PrintWriter используют для более эффективной записи символов, массивов, строк и других типов данных:

BufferedWriter writer = new BufferedWriter(new FileWriter("report.txt"));
PrintWriter printer = new PrintWriter(new BufferedWriter(new FileWriter("log.txt")));

В последней строке три уровня: FileWriter пишет в файл, BufferedWriter копит символы в буфере и отдаёт их крупными порциями, PrintWriter добавляет удобные println() и printf(). Основные обёртки java.io:

Обёртка Что оборачивает Что добавляет Нюанс
BufferedInputStream / BufferedOutputStream InputStream / OutputStream Буфер 8192 байта: меньше обращений к диску Без flush()/close() записанное остаётся в буфере
BufferedReader Reader Буфер 8192 символа и метод readLine() readLine() возвращает null в конце потока, символ перевода строки отбрасывается
BufferedWriter Writer Буфер и метод newLine() Данные попадают в файл только при заполнении буфера, flush() или close()
PrintWriter Writer или OutputStream print(), println(), printf() Не бросает IOException: ошибку записи узнают только через checkError()
InputStreamReader InputStream Мост байты → символы с заданной кодировкой Без явного Charset берётся кодировка по умолчанию
OutputStreamWriter OutputStream Мост символы → байты с заданной кодировкой То же: кодировку лучше указывать явно

Конструкторы и методы буферизованных классов и PrintWriter с примерами — в уроке Классы символьных потоков.

Цепочка обёрток: от байтов к строкам

Чтобы читать текстовый файл построчно в нужной кодировке, байтовый поток оборачивают в InputStreamReader (мост от байтов к символам), а его — в BufferedReader (буфер и readLine()). Для записи цепочка зеркальная: FileOutputStream → OutputStreamWriter → BufferedWriter → PrintWriter.

Пример: программа записывает две строки в файл в кодировке UTF-8 через цепочку из четырёх потоков, а затем читает их обратно через цепочку из трёх.

import java.io.*;
import java.nio.charset.StandardCharsets;

public class WrappingDemo {
    public static void main(String[] args) throws IOException {
        // Запись: PrintWriter -> BufferedWriter -> OutputStreamWriter -> FileOutputStream
        try (PrintWriter printer = new PrintWriter(
                new BufferedWriter(
                        new OutputStreamWriter(
                                new FileOutputStream("notes.txt"), StandardCharsets.UTF_8)))) {
            printer.println("Привет, поток!");
            printer.printf("%d + %d = %d%n", 2, 3, 2 + 3);
        }

        // Чтение: BufferedReader -> InputStreamReader -> FileInputStream
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(
                        new FileInputStream("notes.txt"), StandardCharsets.UTF_8))) {
            String line;
            int number = 1;
            while ((line = reader.readLine()) != null) {
                System.out.println(number++ + ": " + line);
            }
        }
    }
}

Вывод программы:

1: Привет, поток!
2: 2 + 3 = 5

Каждое звено отвечает ровно за одно: FileOutputStream знает только про байты и файл, OutputStreamWriter — про кодировку, BufferedWriter — про буфер, PrintWriter — про форматирование. Если в файле окажется не UTF-8, а, например, windows-1251, достаточно поменять один аргумент Charset.

Кодировка по умолчанию

Если не указать Charset, FileReader, FileWriter и InputStreamReader используют кодировку по умолчанию. До Java 17 включительно она зависела от ОС (на русской Windows — windows-1251), поэтому файл в UTF-8 читался «кракозябрами». С Java 18 (JEP 400) по умолчанию везде UTF-8. Начиная с Java 11 у FileReader и FileWriter есть конструкторы с параметром Charset — явная кодировка делает код одинаковым на любой версии и ОС.

Какую обёртку закрывать

Закрывать нужно только самую внешнюю обёртку: её метод close() сначала сбрасывает свой буфер (flush()), а затем закрывает вложенный поток, тот — следующий, и так до файла. Закрытие внутреннего потока вместо внешнего — одна из частых ошибок: данные из буфера обёртки теряются.

Что выведет этот код и что окажется в файле?

FileWriter fileWriter = new FileWriter("log.txt");
BufferedWriter writer = new BufferedWriter(fileWriter);
writer.write("строка");
fileWriter.close();   // закрыли внутренний поток
writer.close();       // пытаемся закрыть обёртку

Результат: файл log.txt пустой, а writer.close() бросает java.io.IOException: Stream closed. Строка «строка» лежала в буфере BufferedWriter; при закрытии обёртка пытается сбросить её во внутренний поток, а тот уже закрыт. Ровно поэтому пустым остался и report.txt из примера в начале урока: там close() не вызвали вовсе, и буфер не был сброшен.

Исправление — закрывать внешнюю обёртку, лучше всего через try-with-resources:

try (BufferedWriter writer = new BufferedWriter(new FileWriter("log.txt"))) {
    writer.write("строка");
} // writer.close(): flush() буфера, затем FileWriter.close()
// Результат: в log.txt записано «строка»

Неочевидный момент

В записи new InputStreamReader(new FileInputStream(f), "UTF8x") файл уже открыт, когда конструктор обёртки бросает UnsupportedEncodingException, и FileInputStream никто не закроет. Если конструктор обёртки может упасть, объявляйте каждое звено отдельным ресурсом: try (FileInputStream in = new FileInputStream(f); InputStreamReader r = new InputStreamReader(in, cs)) — try-with-resources закроет их в обратном порядке.

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

Можно ли скопировать картинку или zip-архив через FileReader и FileWriter?

Нет. Символьные потоки декодируют байты в символы по кодировке, и последовательности байтов, недопустимые для неё, заменяются символом-заменителем — файл окажется повреждённым. Двоичные данные копируют байтовыми потоками FileInputStream/FileOutputStream (см. урок Типы байтовых потоков).

Где в java.io видно, что обёртки — это паттерн «декоратор»?

В базовых классах FilterInputStream, FilterOutputStream, FilterReader и FilterWriter: они хранят ссылку на вложенный поток и по умолчанию передают ему все вызовы. От FilterInputStream наследуются, например, BufferedInputStream и DataInputStream. Как обёртки складываются в цепочку — в разделе Упаковка (wrapping) потоков.

Когда вызывать flush(), если есть close()?

close() сам вызывает flush(), поэтому в конце работы отдельный вызов не нужен. flush() нужен, когда поток остаётся открытым, а данные должны дойти до приёмника сразу: запись лога в долго работающей программе, ответ по сети, вывод прогресса.

Чем java.io отличается от java.nio?

java.io построен на потоках и блокирующих вызовах read()/write(). java.nio (Java 1.4) добавил каналы, буферы и неблокирующий ввод-вывод, а NIO.2 (Java 7) — классы Path и Files. При этом Files.newBufferedReader(path) возвращает обычный BufferedReader в UTF-8, так что знание потоков java.io нужно и при работе с NIO.

Comments

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