I/O ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-10-31

Предопределенные потоки ввода-вывода в Java: System.in, System.out, System.err

Студент пишет int n = System.in.read();, вводит с клавиатуры 5 и получает n = 53. Следующий вызов read() возвращает 10, хотя он больше ничего не вводил. Ошибки нет: System.in отдаёт байты, а не числа — 53 это код символа '5', а 10 — перевод строки от клавиши Enter. Как устроены стандартные потоки Java и как с ними правильно работать, разберём ниже.

Предопределенные (стандартные) потоки ввода-вывода в Java — это три готовых потока, которые JVM создаёт при запуске программы и хранит в полях класса java.lang.System: System.in (стандартный ввод), System.out (стандартный вывод) и System.err (стандартный поток ошибок). Пакет java.lang импортируется автоматически, поэтому эти потоки доступны в любой программе без import и без создания объектов.

1. Класс System и три стандартных потока

Класс System из пакета java.lang хранит ссылки на три стандартных потока в полях in, out и err, объявленных как public static final. Они связаны со стандартными дескрипторами процесса 0, 1 и 2, которые операционная система по умолчанию направляет в терминал: ввод — с клавиатуры, вывод и ошибки — на экран.

Кроме потоков, System инкапсулирует и другие свойства исполняющей среды: текущее время (currentTimeMillis(), nanoTime()), системные свойства (getProperty()), переменные окружения (getenv()), разделитель строк (lineSeparator()). Создать объект System нельзя — все его члены статические.

Поле Тип Дескриптор ОС По умолчанию связан с Для чего
System.in InputStream 0 (stdin) Клавиатурой в терминале Ввод данных пользователем
System.out PrintStream 1 (stdout) Экраном терминала Обычный вывод программы
System.err PrintStream 2 (stderr) Экраном терминала Сообщения об ошибках и диагностика

System.in — байтовый поток ввода (его базовые методы описаны в уроке Типы байтовых потоков), System.out и System.err — байтовые потоки вывода с удобными методами печати. Общее понятие потока данных — в уроке Потоки данных в Java.

2. System.out: вывод в консоль через PrintStream

System.out — объект класса PrintStream, связанный со стандартным выводом. Запись System.out.println("Hi") читается так: у класса System берётся статическое поле out, и у этого объекта вызывается метод println(), который печатает значение и переводит строку.

Методы PrintStream превращают в текст значения любых типов и, в отличие от большинства потоков, не выбрасывают IOException: ошибка записи только выставляет внутренний флаг.

Метод Что делает Граничный случай
print(x) Печатает значение любого типа без перевода строки Переменная String со значением null печатается как null; литерал print(null) не компилируется (неоднозначно: print(char[]) или print(String)); print((char[]) null) — NullPointerException
println(x), println() Печатает значение и разделитель строк ОС (System.lineSeparator()) println(char[]) печатает символы массива, а println("" + chars) — [C@ и хеш
printf(format, args...), format(...) Форматированный вывод по шаблону: %d, %s, %.2f, %n Неверный тип аргумента — IllegalFormatConversionException; дробная часть зависит от локали (3,14 в русской)
write(int b), write(byte[] buf, int off, int len) Записывает сырые байты без преобразования write(int) берёт только младшие 8 бит
flush() Выталкивает буфер в приёмник Для System.out обычно не нужен — включён autoflush
checkError() Сбрасывает буфер и возвращает true, если была ошибка записи Единственный способ узнать о сбое: исключений методы печати не бросают
public class PrintDemo {
    public static void main(String[] args) {
        String name = "Анна";
        int age = 25;
        double score = 4.567;

        System.out.print("Имя: ");
        System.out.println(name);
        System.out.println("Возраст: " + age);
        System.out.printf("Балл: %.1f, возраст через год: %d%n", score, age + 1);
    }
}
// Вывод (русская локаль):
// Имя: Анна
// Возраст: 25
// Балл: 4,6, возраст через год: 26

JVM создаёт System.out с автоматическим сбросом буфера (autoflush), поэтому текст появляется в консоли сразу после println(), без вызова flush(). Обратная сторона — при печати сотен тысяч строк в цикле каждый вызов обращается к ОС, и вывод становится узким местом (см. FAQ). Символьный аналог PrintStream — PrintWriter, их различия разобраны в уроке Классы символьных потоков.

3. System.err: чем отличается от System.out

System.err — такой же PrintStream с теми же методами, но он пишет в отдельный канал ОС — стандартный поток ошибок (дескриптор 2). Разделение нужно, чтобы ошибки можно было отделить от полезного вывода: при перенаправлении System.out в файл сообщения System.err по-прежнему видны на экране.

public class ErrDemo {
    public static void main(String[] args) {
        System.out.println("Начало обработки");
        System.err.println("Ошибка: файл data.csv не найден");
        System.out.println("Конец обработки");
    }
}

Если запустить программу с перенаправлением стандартного вывода в файл, на экране останется только строка ошибки:

java ErrDemo > out.txt
# На экране:   Ошибка: файл data.csv не найден
# В out.txt:   Начало обработки
#              Конец обработки

java ErrDemo > out.txt 2> err.txt   # вывод и ошибки в разные файлы
java ErrDemo > all.txt 2>&1         # оба потока в один файл

В System.err пишет и метод e.printStackTrace() без аргументов, поэтому трассировки исключений в IDE обычно подсвечены красным. В реальных приложениях вместо прямой печати в System.err используют логгер (java.util.logging, SLF4J + Logback), который сам решает, куда и в каком формате писать сообщения.

Почему строки перемешались

В консоли IDE красная строка из System.err иногда появляется раньше строки System.out, напечатанной до неё. Это не ошибка Java: out и err — два независимых канала ОС, и IDE читает их параллельно. Если важен порядок сообщений, пишите их в один поток.

4. System.in: чтение с клавиатуры

System.in — байтовый поток ввода (тип InputStream), связанный со стандартным вводом. Метод System.in.read() читает один байт и возвращает его как int от 0 до 255, а в конце ввода — -1. Чтобы получить строки и числа, поток оборачивают в BufferedReader или используют Scanner.

Вызов read() блокирует программу, пока пользователь не нажмёт Enter: терминал передаёт введённую строку целиком, вместе с символом конца строки.

Пример 1. Побайтовое чтение через System.in.read()

import java.io.IOException;

public class ReadBytes {
    public static void main(String[] args) throws IOException {
        System.out.print("Введите цифру: ");
        int first = System.in.read();
        int second = System.in.read();
        System.out.println("first = " + first + ", как символ: " + (char) first
                + ", как число: " + (first - '0'));
        System.out.println("second = " + second);
    }
}
// Ввод: 5 и Enter
// Вывод (Linux, macOS):
// Введите цифру: 5
// first = 53, как символ: 5, как число: 5
// second = 10
// В Windows second = 13: Enter даёт пару \r\n, и первым приходит \r

Это и есть случай из начала урока: 53 — код символа '5', а «лишний» байт — перевод строки, который остался в потоке и достался второму вызову. Кириллица в UTF-8 занимает 2 байта на букву, поэтому побайтовое чтение подходит только для простейших случаев.

Пример 2. Чтение строк и чисел через BufferedReader

Для чтения строк System.in оборачивают в InputStreamReader (переводит байты в символы) и BufferedReader (даёт метод readLine()). Как работает этот мост и почему важна кодировка — в разделе InputStreamReader и OutputStreamWriter.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ConsoleSum {
    public static void main(String[] args) throws IOException {
        BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
        System.out.print("Первое число: ");
        int a = Integer.parseInt(console.readLine().trim());
        System.out.print("Второе число: ");
        int b = Integer.parseInt(console.readLine().trim());
        System.out.println("Сумма: " + (a + b));
        // console не закрываем: вместе с ним закроется System.in
    }
}
// Ввод: 7 и 35
// Вывод:
// Первое число: 7
// Второе число: 35
// Сумма: 42
// Ввод «abc»: java.lang.NumberFormatException: For input string: "abc"
// Ctrl+D вместо ввода: readLine() вернёт null, и вызов trim() бросит NullPointerException

Если нужно разбирать ввод сразу на числа и слова (nextInt(), nextDouble()), удобнее класс Scanner — он разобран в уроке Класс Scanner.

Как закончить ввод

Конец стандартного ввода с клавиатуры передают сочетанием Ctrl+D в Linux и macOS или Ctrl+Z и Enter в Windows. После этого System.in.read() возвращает -1, а readLine() — null. Если ввод перенаправлен из файла (java App < input.txt), конец файла и есть конец ввода.

5. Перенаправление: setIn, setOut, setErr

Стандартные потоки можно заменить прямо из программы статическими методами System.setIn(), System.setOut() и System.setErr(). После вызова весь код, который пишет в System.out или читает System.in, работает с новым потоком: файлом, буфером в памяти, сетевым соединением.

Метод Что делает Типичное применение
System.setIn(InputStream in) Подменяет стандартный ввод Подставить заранее заготовленный ввод в тесте: new ByteArrayInputStream("42\n".getBytes())
System.setOut(PrintStream out) Подменяет стандартный вывод Перехватить вывод метода в тесте, записать вывод в файл
System.setErr(PrintStream err) Подменяет поток ошибок Собрать сообщения об ошибках и трассировки в журнал

Поля in, out, err объявлены final, и присвоить им значение обычным кодом нельзя: System.out = ... не скомпилируется. Методы setXxx() меняют их через нативный код JVM — это редкое исключение из правила «final-поле не меняется».

Пример 3. Перехват вывода в строку

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

public class CaptureOutput {
    public static void main(String[] args) {
        PrintStream original = System.out;              // сохраняем, чтобы вернуть
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        System.setOut(new PrintStream(buffer, true));
        try {
            System.out.println("Эта строка не попадёт в консоль");
        } finally {
            System.setOut(original);                    // восстанавливаем всегда
        }
        System.out.println("Перехвачено: " + buffer.toString().trim());
    }
}
// Вывод:
// Перехвачено: Эта строка не попадёт в консоль

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

Пример 4. Подмена ввода и запись ошибок в файл

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

public class RedirectDemo {
    public static void main(String[] args) throws IOException {
        System.setIn(new ByteArrayInputStream("42\n".getBytes(StandardCharsets.UTF_8)));
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
        System.out.println("Прочитано: " + in.readLine());

        PrintStream originalErr = System.err;
        try (PrintStream log = new PrintStream(
                new FileOutputStream("errors.log", true), true, StandardCharsets.UTF_8)) { // Java 10+
            System.setErr(log);
            System.err.println("Это сообщение уйдёт в errors.log");
            new IllegalStateException("тест").printStackTrace(); // трассировка тоже в файл
        } finally {
            System.setErr(originalErr); // иначе System.err будет указывать на закрытый поток
        }
    }
}
// Вывод в консоль:
// Прочитано: 42
// В errors.log дописаны строка сообщения и трассировка java.lang.IllegalStateException: тест

Второй параметр FileOutputStream (true) включает дописывание в конец файла, второй параметр PrintStream (true) — autoflush. Перенаправить потоки можно и без изменения кода — средствами оболочки (>, 2>, <), как показано в разделе о System.err.

6. Что чаще всего упускают при работе с консолью

Ошибки со стандартными потоками почти всегда тихие: программа компилируется и работает, но печатает не то, что ожидалось, или перестаёт читать ввод. Большинство из них связано с тем, что System.in отдаёт байты, а вывод зависит от локали, кодировки и перегрузки метода.

Ошибка Что происходит Как правильно
int n = System.in.read(); для ввода числа На ввод 5 получаем 53, на 12 — 49, а остаток строки достаётся следующему чтению Читать строку через readLine() и разбирать Integer.parseInt()
Закрыть BufferedReader или Scanner, обёрнутый вокруг System.in (в том числе через try-with-resources) Закрывается сам System.in; следующее чтение даёт IOException: Stream closed (у Scanner — NoSuchElementException), открыть его заново нельзя Создать одну обёртку на всю программу и не закрывать её
System.out.close() Весь последующий вывод молча пропадает: PrintStream исключений не бросает Не закрывать стандартные потоки; для своих потоков — try-with-resources
System.setOut(...) без восстановления Вывод остальной программы или следующих тестов уходит в чужой поток Сохранить исходный поток и вернуть его в finally
printf("%.2f", x) для машинного формата (CSV, JSON) В русской локали печатается 3,14 вместо 3.14 System.out.printf(Locale.ROOT, "%.2f%n", x)

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

char[] word = {'J', 'a', 'v', 'a'};
System.out.println(word);
System.out.println("Слово: " + word);
System.out.printf("%.2f%n", 3.14159);

Ответ (русская локаль):

Java
Слово: [C@1b6d3586
3,14

Первая строка вызывает перегрузку println(char[]), которая печатает символы массива. Во второй строке массив сначала участвует в конкатенации и превращается в строку через Object.toString(): [C — обозначение типа char[], после @ — хеш-код, который меняется от запуска к запуску. Исправление — "Слово: " + new String(word) или String.valueOf(word). Третья строка использует локаль по умолчанию, поэтому в русской системе разделитель дробной части — запятая, а в английской будет 3.14.

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

Почему System.out.println тормозит программу в больших циклах?

System.out создан с autoflush: каждый println() сбрасывает буфер и обращается к ОС, а методы PrintStream ещё и синхронизированы. Миллион вызовов println() может занимать секунды. Для большого вывода соберите текст в StringBuilder и напечатайте один раз или пишите через new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out))) с одним flush() в конце.

Как прочитать пароль с консоли, чтобы он не отображался на экране?

Через класс java.io.Console: char[] pwd = System.console().readPassword("Пароль: ");. Вводимые символы не отображаются, а пароль возвращается массивом char[], который после проверки можно затереть. System.console() может вернуть null, если программа запущена без терминала (часто в IDE или при перенаправлении ввода), поэтому результат нужно проверять; с Java 22 есть метод Console.isTerminal().

Почему кириллица в консоли Windows выводится «кракозябрами»?

Консоль Windows по умолчанию работает в кодировке cp866, а Java кодирует вывод System.out по-своему. Переход на UTF-8 по умолчанию в Java 18 (JEP 400) стандартные потоки не затронул: с Java 19 их кодировку задают свойства stdout.encoding и stderr.encoding. Обычное решение — переключить консоль командой chcp 65001 и запустить программу с -Dstdout.encoding=UTF-8, либо создать свой поток: new PrintStream(new FileOutputStream(FileDescriptor.out), true, StandardCharsets.UTF_8).

Что такое IO.println() и заменит ли он System.out.println()?

В Java 25 (JEP 512) появился класс java.lang.IO с методами print(), println() и readln() для простых программ и обучения: String name = IO.readln("Имя: ");. Внутри он работает поверх тех же стандартных потоков, поэтому System.out, System.err и System.in остаются основой: перенаправление, перехват вывода в тестах и форматирование через printf() делаются только через них.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.