W3docs

Java PrintStream

Как PrintStream обеспечивает работу System.out и System.err и как использовать его для форматированного вывода байтов.

PrintStream — это класс, который работает под вашим кодом с самой первой главы. System.out — это PrintStream. System.err — это PrintStream. Каждый написанный вами System.out.println(...) проходил через этот класс.

Его интерфейс совпадает с PrintWriter, который вы только что изучили — print, println, printf, format — и то же поведение с «проглатыванием» исключений. Разница в том, на чём он основан: PrintStream расширяет OutputStream (байты), тогда как PrintWriter расширяет Writer (символы). При записи в файлы различие между байтами и символами, рассмотренное ранее в этой части, по-прежнему актуально: символы поступают, символы выводятся, а кодировка находится на границе.

Почему два класса с одинаковым API?

История. В Java 1.0 был PrintStream, но никакой иерархии Writer вообще — весь «вывод» шёл в байтовый поток. В Java 1.1 появилась иерархия Reader/Writer для правильной работы с символами и был добавлен PrintWriter, чтобы код записи в файлы мог использовать тот же API для символов. PrintStream нельзя было упразднить, поскольку System.out и System.err уже имели тип PrintStream в опубликованных API, и их изменение сломало бы все программы в мире.

Так что оба существуют. Практическое правило:

  • Используйте PrintWriter для файлов. Символьно-ориентированная иерархия — это то место, где должна находиться кодировка.
  • Используйте PrintStream, когда это необходимо — то есть когда целью является System.out/System.err, или когда вы пишете в OutputStream, который не хотите оборачивать.

Случаи «необходимости» редки. В большинстве случаев можно сделать так:

PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8);
out.println("hello");

и забыть о существовании PrintStream.

API

Идентичен PrintWriter:

void print(boolean | char | int | long | float | double | String | Object);
void println(...);                                  // adds the platform line separator
PrintStream printf(String format, Object... args);
PrintStream format(String format, Object... args);
PrintStream append(CharSequence s);

Плюс унаследованные методы OutputStream (write(int), write(byte[]), flush, close). Та же ловушка из BufferedWriter и PrintWriter применима здесь: println пишет System.lineSeparator(), что равно \r\n на Windows. Пишите \n явно, когда вывод должен быть переносимым.

Конструкторы

new PrintStream(OutputStream out);                                   // platform default charset
new PrintStream(OutputStream out, boolean autoFlush, Charset cs);    // explicit charset
new PrintStream(File file, Charset charset);                          // open a file
new PrintStream(String filename, Charset charset);

Как и в случае с PrintWriter, конструкторы без кодировки используют кодировку JVM по умолчанию — тот же риск переносимости, описанный в главе о символьных потоках. Всегда передавайте кодировку.

Флаг autoFlush имеет ту же семантику, что и в PrintWriter: когда включён, println, printf, format и write(byte[], int, int) при переводе строки вызывают сброс буфера. print этого не делает. По умолчанию отключён.

«Проглоченный» IOException (по-прежнему)

Та же конструкция, что и в PrintWriter. Ни один из методов print/println/printf не выбрасывает IOException. Неудачная запись устанавливает флаг ошибки, который вы читаете с помощью checkError(). Компромисс тот же: удобно для быстрого кода, опасно, если не проверять.

Для System.out/System.err в частности, «проглатывание» — правильное решение: нет ничего полезного, что можно сделать, когда запись в терминал завершается неудачей. Для PrintStream, работающего с файлом, предпочтите PrintWriter или проверяйте checkError() перед закрытием.

System.out и System.err

Это два экземпляра PrintStream, создаваемые при запуске JVM. Они оборачивают файловые дескрипторы stdout и stderr операционной системы. Кодировка символов следует за stdout.encoding (Java 18+) или file.encoding (более старые версии), поэтому перенаправленный по конвейеру вывод иногда превращается в «кракозябры» на консоли Windows — кодовая страница консоли не совпадает с тем, что JVM считает кодировкой.

Вы можете заменить их с помощью System.setOut(PrintStream) и System.setErr(PrintStream), что иногда полезно для захвата вывода в тестах:

ByteArrayOutputStream captured = new ByteArrayOutputStream();
PrintStream original = System.out;
System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8));
try {
  runTheCodeUnderTest();
  assertEquals("expected\n", captured.toString(StandardCharsets.UTF_8));
} finally {
  System.setOut(original);
}

В производственном коде не трогайте их. Фреймворки логирования (java.util.logging, SLF4J/Logback) используют иной, структурированный подход к записи диагностического вывода.

print(Object) и null

Тонкое поведение, общее с PrintWriter: print(Object o) вызывает String.valueOf(o), который возвращает четырёхсимвольную строку "null" для нулевой ссылки вместо того, чтобы выбросить NullPointerException. Именно поэтому

System.out.println(maybeNullList);                  // prints "null", not NPE

работает. Удобно для быстрого логирования; вводит в заблуждение, если вы записываете строку обратно в файл данных, который будете разбирать позже — "null" как строка неотличима от буквального слова «null».

write(int) пишет байт, а не символ

PrintStream — это OutputStream. Унаследованный write(int b) записывает младший байт:

System.out.write(65);                              // writes 'A' — the byte 0x41
System.out.write('é');                              // writes a single byte 0xE9 — NOT UTF-8 for 'é'

Вторая строка неверна на терминале UTF-8 — 'é' занимает два байта в UTF-8 (0xC3 0xA9), а вы записали один. Не используйте write(int) в PrintStream для символов; используйте print/println, которые проходят через настроенную кодировку.

Пример с перехватом и проверкой System.out

Приведённая ниже программа захватывает System.out в ByteArrayOutputStream, чтобы вы могли точно увидеть, какие байты JVM выдаёт при вызове println. Она запускает один и тот же println("Café") с двумя разными кодировками, чтобы сделать поведение при кодировании наглядным, демонстрирует checkError() на сломанном потоке и наконец показывает разницу между print(Object) для нулевой ссылки и явной проверкой на null.

java— editable, runs on the server

Что следует вынести из запуска:

  • System.setOut(new PrintStream(buffer, ...)) захватил то, что иначе ушло бы в консоль. Тесты постоянно используют этот паттерн. Восстановите оригинал перед выводом отчёта — иначе отчёт тоже попадёт в буфер, что приведёт к путанице.
  • Строка «Café» выдала 5 байт в UTF-8 (43 61 66 C3 A9) и 4 байта в ISO-8859-1 (43 61 66 E9). Одни входные данные, разная ширина в байтах, оба варианта корректны — кодировка — это отображение байт → символ, и PrintStream соблюдает кодировку, переданную его конструктору. Конструктор без кодировки взял бы ту, с которой JVM случайно запустилась.
  • Блок со сломанным потоком доказал «проглатывание»: println вернул управление нормально, базовый IOException исчез, и только checkError() позволял узнать, что запись провалилась. Тот же контракт, что и в PrintWriter. Если вас интересует сбой, вы должны спросить.
  • Вывод нулевой ссылки дал четырёхсимвольную строку null, а не NullPointerException. Именно так работает println(someList), даже когда someList равен null — удобно, но это означает, что однажды записанное на диск буквальное слово «null» неотличимо от нулевой ссылки. Используйте Objects.requireNonNull или явную проверку на null на границе, если это различие важно.
  • В примере нигде не использовался PrintWriter. Для System.out он не нужен — PrintStream — это тип, который Java уже дала вам, API идентичен, а поведение автосброса при println — это именно то, что нужно в терминале.

Что дальше

Первые тринадцать глав этой части охватили все виды потокового ввода-вывода: байты, символы, буферизацию, примитивы, форматированный текст. Все они передают содержимое — байты и символы. Следующая глава, Java Serialization, посвящена передаче графов объектов — целой связанной структуры ссылок, записанной в поток и восстановленной на другой стороне, с единственной аннотацией в классе.

Практика

Практика
`PrintWriter` и `PrintStream` имеют почти идентичные API (`print`, `println`, `printf`). Какой из них следует предпочесть при записи текста в файл и почему?
`PrintWriter` и `PrintStream` имеют почти идентичные API (`print`, `println`, `printf`). Какой из них следует предпочесть при записи текста в файл и почему?
Was this page helpful?