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.
Что следует вынести из запуска:
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, посвящена передаче графов объектов — целой связанной структуры ссылок, записанной в поток и восстановленной на другой стороне, с единственной аннотацией в классе.