Поток ввода-вывода (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
Разбор по шагам:
- Цепочка оберток.
FileOutputStreamпишет байты,OutputStreamWriterпревращает символы в байты по UTF-8,BufferedWriterдобавляет буфер. Каждый слой добавляет одну возможность (паттерн «декоратор»). - try-with-resources. Объявленное в
try (...)закрывается при выходе из блока, даже при исключении; закрытие внешней обертки закрывает и вложенный поток. - Конец потока.
readLine()возвращаетnull, аread()байтового потока возвращает-1— это сигнал, что данных больше нет. - Байты против символов. В строке 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?
Нет. Его открывает среда выполнения, а после закрытия вывод в консоль через него до конца программы пропадет.



