Предопределенные потоки ввода-вывода в 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() делаются только через них.
Комментарии