Потоки ввода-вывода (streams): виды и примеры на Java и Python

Потоки ввода-вывода (streams): виды и примеры на Java и Python Полезное

Поток ввода-вывода (stream) — это абстракция последовательного чтения или записи данных: программа берет байты или символы из источника (файл, сеть, консоль, память) или отдает их в приемник, не зная, что физически стоит на другом конце. Поток данных не надо путать с потоком выполнения (thread) — это параллельная ветка кода, и с Stream API в Java 8+ — конвейером обработки коллекций.

Ниже разберем виды потоков ввода-вывода, буферизацию, кодировки и закрытие потоков. Примеры проверены на Java 25 (Eclipse Temurin 25.0.4) и Python 3 на Linux 23.09.2026.

Stream, thread и Stream API: три разных «потока»

Слово «поток» в русском обозначает три разные сущности.

Термин Что это Пример в Java
Поток данных, I/O stream Последовательность байтов или символов между программой и источником/приемником InputStream, Reader, System.out
Поток выполнения, thread Отдельная ветка исполнения кода внутри процесса Thread, ExecutorService
Stream API Конвейер операций над элементами коллекции: map, filter, collect list.stream()

Все три в одной программе:

import java.util.List;

public class ThreeStreams {
    public static void main(String[] args) throws InterruptedException {
        // Stream API: конвейер обработки коллекции, файлов и сети тут нет
        List<Integer> lengths = List.of("a", "bb", "ccc").stream()
                .map(String::length)
                .toList();
        System.out.println(lengths);

        // Thread: поток выполнения, отдельная ветка кода
        Thread worker = new Thread(() ->
                System.out.println("Работает " + Thread.currentThread().getName()));
        worker.start();
        worker.join();

        // I/O stream: System.out - выходной поток данных
        System.out.println(System.out.getClass().getSimpleName());
    }
}

Вывод:

[1, 2, 3]
Работает Thread-0
PrintStream

Дальше речь только о первой строке таблицы.

Виды потоков ввода-вывода

Потоки классифицируют по двум независимым признакам: направлению и единице данных.

  • По направлению: входной поток (input) — программа читает из него; выходной (output) — программа пишет в него.
  • По единице данных: байтовый поток передает сырые байты (картинки, архивы, любой двоичный формат); символьный — текст, и на его границе байты превращаются в символы по кодировке.

В Java эти признаки дают четыре базовых абстрактных класса пакета java.io:

Входной Выходной
Байтовый InputStream OutputStream
Символьный Reader Writer

Источник задает подкласс: FileInputStream — файл, ByteArrayInputStream — массив в памяти, socket.getInputStream() — сеть. Стандартные потоки программы: stdin (System.in), stdout (System.out), stderr (System.err).

Полный пример на Java: запись и чтение файла

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

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

public class StreamsDemo {
    public static void main(String[] args) throws IOException {
        File file = new File("notes.txt");

        // 1. Выходной символьный поток: пишем текст в файл в UTF-8
        try (BufferedWriter out = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
            out.write("Привет, поток!");
            out.newLine();
            out.write("Вторая строка");
            out.newLine();
        } // здесь close(): буфер сброшен на диск, файл закрыт

        // 2. Входной символьный поток: читаем построчно
        try (BufferedReader in = new BufferedReader(
                new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
            String line;
            int n = 0;
            while ((line = in.readLine()) != null) {
                n++;
                System.out.println(n + ": " + line);
            }
        }

        // 3. Входной байтовый поток: считаем байты того же файла
        try (InputStream raw = new BufferedInputStream(new FileInputStream(file))) {
            int count = 0;
            while (raw.read() != -1) {
                count++;
            }
            System.out.println("Байт в файле: " + count);
        }
        System.out.println("Символов в первой строке: " + "Привет, поток!".length());
    }
}

Вывод (Linux):

1: Привет, поток!
2: Вторая строка
Байт в файле: 52
Символов в первой строке: 14

Разбор по шагам:

  1. Цепочка оберток. FileOutputStream пишет байты, OutputStreamWriter превращает символы в байты по UTF-8, BufferedWriter добавляет буфер. Каждый слой добавляет одну возможность (паттерн «декоратор»).
  2. try-with-resources. Объявленное в try (...) закрывается при выходе из блока, даже при исключении; закрытие внешней обертки закрывает и вложенный поток.
  3. Конец потока. readLine() возвращает null, а read() байтового потока возвращает -1 — это сигнал, что данных больше нет.
  4. Байты против символов. В строке 14 символов, а в файле 52 байта: кириллица в UTF-8 занимает 2 байта на букву, пробел и знаки — 1 байт. 52 — для Linux и macOS; newLine() пишет системный разделитель, в Windows это \r\n и 54 байта.

Короткий путь — java.nio.file.Files: Files.newBufferedReader(path) и Files.readAllLines(path) по умолчанию читают UTF-8. Граница: readAllLines грузит весь файл в память.

Буферизация и закрытие: частая ошибка

Буфер копит данные в памяти и отправляет на диск или в сеть пачкой, а не по символу. Обратная сторона — пока буфер не сброшен, данных в файле нет.

Неверно — поток не закрыт:

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

public class LostData {
    public static void main(String[] args) throws IOException {
        BufferedWriter out = new BufferedWriter(
                new FileWriter("lost.txt", StandardCharsets.UTF_8));
        out.write("Эта строка потеряется");
        // забыли out.close()
        System.out.println("Размер файла: " + new File("lost.txt").length());
    }
}

Результат: Размер файла: 0, и после завершения программы файл так и остается пустым — JVM не сбрасывает чужие буферы при выходе.

Исправление — try-with-resources:

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

public class SavedData {
    public static void main(String[] args) throws IOException {
        try (BufferedWriter out = new BufferedWriter(
                new FileWriter("saved.txt", StandardCharsets.UTF_8))) {
            out.write("Эта строка сохранится");
        }
        System.out.println("Размер файла: " + new File("saved.txt").length());
    }
}

Результат: Размер файла: 40. Метод flush() сбрасывает буфер без закрытия — он нужен, когда поток живет долго (лог, сетевой протокол), а данные должны уйти сейчас.

Кодировка: где байты становятся символами

Символьный поток всегда работает с какой-то кодировкой. Если ее не указать, Java берет кодировку по умолчанию: начиная с Java 18 это UTF-8 на всех платформах, в более старых версиях — кодировка ОС (в русской Windows часто windows-1251). Поэтому файл, записанный в одной среде, может прочитаться «кракозябрами» в другой.

Правило простое: для текстовых файлов указывайте кодировку явно (StandardCharsets.UTF_8), для двоичных данных используйте байтовые потоки и не пропускайте их через Reader.

Потоки ввода-вывода в Python

В Python то же деление задает режим функции open(): текстовый режим возвращает символьный поток, режим "b" — байтовый. Роль try-with-resources играет конструкция with.

import io

with open("notes.txt", "w", encoding="utf-8") as f:   # выходной текстовый поток
    f.write("Привет, поток!\nВторая строка\n")

with open("notes.txt", encoding="utf-8") as f:        # входной текстовый поток
    for n, line in enumerate(f, 1):
        print(n, line.rstrip("\n"))

with open("notes.txt", "rb") as f:                    # входной байтовый поток
    data = f.read()
print(len(data), type(data).__name__)

buf = io.StringIO()                                   # поток в памяти
print("в память", file=buf)
print(repr(buf.getvalue()))

Вывод (Linux):

1 Привет, поток!
2 Вторая строка
52 bytes
'в память\n'

Байтовый поток отдает bytes, текстовый — str. В Windows текстовый режим при записи превращает \n в \r\n, поэтому размер файла там будет 54 байта. io.StringIO и io.BytesIO — потоки в памяти, удобны в тестах.

Если не получилось

Симптом Вероятная причина Что сделать
Файл создан, но пустой Поток не закрыт, данные остались в буфере try-with-resources в Java, with в Python
Вместо кириллицы «кракозябры» Записали в одной кодировке, читаете в другой Явно указать UTF-8 и при записи, и при чтении
Испорчена картинка или архив после копирования Двоичные данные прошли через символьный поток Копировать байтовым потоком (InputStream, режим "rb")
Too many open files Потоки открываются в цикле и не закрываются Закрывать каждый поток, проверить все ветки с исключениями

Если путь к файлу приходит от пользователя, не подставляйте его в new File(...) напрямую: проверьте, что итоговый путь после нормализации лежит внутри разрешенного каталога, иначе строка вида ../../ откроет чужой файл.

Выводы

  • Поток ввода-вывода — последовательное чтение или запись данных независимо от источника; это не поток выполнения (thread) и не Stream API.
  • Два признака классификации: входной/выходной и байтовый/символьный; в Java им соответствуют InputStream, OutputStream, Reader, Writer.
  • Потоки собираются цепочкой оберток: источник, преобразование байтов в символы, буфер.
  • Буферизованный поток нужно закрывать, иначе данные теряются; в Java — try-with-resources, в Python — with.
  • Для текста кодировку задают явно, двоичные данные читают только байтовым потоком.

Где применяется / связь с практикой

Освойте тему на практике

Потоки ввода-вывода есть почти в любой Java-программе: конфиги, CSV, загрузка файлов, сокеты, логи. Если хотите системно пройти основы языка вместе с java.io, исключениями и коллекциями, посмотрите курс Java Developer. Basic. Чтобы оценить формат обучения, можно начать с бесплатных открытых уроков.

FAQ

Чем java.io отличается от java.nio?
java.io построен на блокирующих потоках, java.nio добавляет каналы, буферы и неблокирующий ввод-вывод.

Можно ли читать один поток повторно?
Обычно нет: поток читается один раз от начала к концу. Некоторые реализации поддерживают mark()/reset(), но для повторного чтения файла проще открыть его заново.

Нужно ли закрывать System.out?
Нет. Его открывает среда выполнения, а после закрытия вывод в консоль через него до конца программы пропадет.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)