Java PrintWriter
Запись форматированного текста в Java с помощью PrintWriter: методы print, println, printf и format.
В главе о символьных потоках был представлен Writer и его основной метод write(String). Этого достаточно для любых задач, но это неудобно: чтобы вывести число, нужно писать w.write(Integer.toString(n)), для вывода строки нужно самостоятельно добавлять символ переноса строки, а форматированный вывод требует вызова String.format на каждый раз. PrintWriter — это декоратор, который устраняет эти неудобства: он добавляет методы print, println, printf и format поверх любого Writer или OutputStream.
Это файловый аналог System.out, который вы используете с первой главы — тот же программный интерфейс, но вывод идёт в файл, а не в консоль.
Что добавляет PrintWriter
void print(boolean | char | int | long | float | double | String | Object);
void println(...); // same overloads, plus the line terminator
PrintWriter printf(String format, Object... args); // String.format under the hood
PrintWriter format(String format, Object... args); // alias for printf
PrintWriter append(CharSequence s); // returns this (for chaining)Плюс унаследованные методы Writer (write, flush, close). Главное преимущество — типизированные перегрузки: можно записывать любой примитив или объект напрямую, и PrintWriter сам вызовет String.valueOf.
try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(path))) {
w.println("header");
w.printf("count = %d%n", 42);
w.printf("rate = %.2f%%%n", 0.875 * 100);
w.println(); // blank line
}close() в блоке try-with-resources сбрасывает буфер; без этого проблема с хвостовым буфером из главы о буферизованных потоках актуальна и для PrintWriter.
Конструкторы
Наиболее полезные, в порядке предпочтения:
PrintWriter(Path file, Charset charset); // Java 10+, opens the file with the charset
PrintWriter(Writer out); // wrap any Writer (typical: a BufferedWriter)
PrintWriter(Writer out, boolean autoFlush); // same, with autoFlush on println/printf
PrintWriter(OutputStream out, boolean autoFlush, Charset charset);Конструктор с Path + Charset — самый простой вариант для «открыть файл и записать в него»:
try (PrintWriter w = new PrintWriter(path.toFile(), StandardCharsets.UTF_8)) {
w.println("hello");
}Он открывает файл, оборачивает его в OutputStreamWriter с указанной кодировкой, оборачивает это в BufferedWriter и возвращает PrintWriter. Стек из четырёх слоёв, который раньше собирался вручную, сворачивается в одну строку.
Всегда указывайте кодировку явно. Конструкторы без кодировки — new PrintWriter("file.txt"), new PrintWriter(outputStream) — используют кодировку JVM по умолчанию, что является той же проблемой переносимости, о которой говорилось в главе о символьных потоках. UTF-8 — правильный выбор по умолчанию.
Проглоченный IOException
PrintWriter отличается от всех других Writer в одном важном отношении: он не бросает IOException. Ни один из методов print, println, printf, write не объявляет его. Если базовый вызов ввода-вывода завершается ошибкой, PrintWriter проглатывает исключение и устанавливает внутренний флаг «ошибки».
Это удобно — можно написать большой блок вызовов println без try/catch вокруг каждого — но это означает, что сбой записи происходит беззвучно. Нужно явно спрашивать:
if (w.checkError()) {
// something went wrong; the underlying IOException was swallowed
throw new IOException("write to " + path + " failed");
}checkError() — единственный способ это узнать. Получить исходный IOException невозможно — к моменту проверки он уже потерян. Поэтому:
- Для вывода в консоль (случай
System.out) проглатывание допустимо: никто не обрабатывает сбой записи в терминал. - Для файлов, где частичная запись — реальная проблема (лог-файл, файл сохранения, генерируемый отчёт), вызывайте
checkError()в конце блока или используйтеBufferedWriterи вызывайтеwriteнапрямую, чтобы исключение распространялось.
Автоматическое сбрасывание буфера
Аргумент autoFlush конструктора управляет тем, будет ли после каждого вызова println, printf или format автоматически вызываться flush(). По умолчанию отключено:
new PrintWriter(out, false) // explicit close/flush only
new PrintWriter(out, true) // flush after every println/printf/formatМетоды print и write никогда не вызывают автоматическое сбрасывание, даже при включённом флаге — только методы вывода строк и форматирования делают это. Поэтому System.out.print("waiting...") может быть невидим, пока выполняется следующее вычисление, а System.out.println("waiting...") появляется немедленно.
Для файлов оставьте автоматическое сбрасывание выключенным и позвольте close() (через try-with-resources) справиться с этим. Для интерактивного лога, за которым вы следите, включите его или вызывайте flush() после каждой партии.
println и разделитель строк платформы
println() записывает System.lineSeparator() — \n в Unix и macOS, \r\n в Windows. То же самое для спецификатора %n в printf. Для терминального вывода это особенность, для файлов данных — недостаток; обсуждение из главы о буферизованных потоках (в разделе BufferedWriter.newLine) применимо здесь в полной мере:
w.printf("row,%d%n", 1); // platform-dependent terminator
w.printf("row,%d\n", 1); // portable \nЕсли выходные данные будут читаться не только на локальной машине, записывайте \n явно.
Сравнение с PrintStream
PrintStream — байт-ориентированный аналог PrintWriter с тем же API, но другим предком. System.out и System.err — это экземпляры PrintStream. Для вывода в файл предпочтительнее PrintWriter: он заставляет задуматься о кодировке символов (конструктор принимает Charset), тогда как PrintStream молча использует кодировку по умолчанию для любого не-ASCII символа.
В следующей главе, Java PrintStream, различия рассматриваются подробно.
Практический пример: запись небольшого CSV-файла
Программа ниже открывает временный файл через PrintWriter, записывает небольшой CSV (заголовок + строки данных), демонстрирует форматирование с помощью printf, вызывает checkError() для проверки успешности записи и показывает поведение «проглатывания исключения» при записи через writer, чей базовый поток был закрыт.
Что следует усвоить из запуска:
- CSV-файл получился именно таким, как задала строка формата
printf.%.2fокруглил цену до двух знаков после запятой;%-10sвыровнял по левому краю в колонке шириной 10 символов;\n(а не%n) сделал символ переноса строки переносимым. Строки формата — главная причина выбиратьPrintWriterвместо обычногоWriter. - Первый блок
try-with-resources свёл стек из четырёх слоёв —Path→FileOutputStream→OutputStreamWriter(UTF-8)→BufferedWriter→PrintWriter— к одному вызову конструктора. Конструктор(File, Charset)— это именно то, что нужно для «открыть файл, записать в него текст, закрыть аккуратно». - Проверка
checkError()выполнялась до вызова close. После выполненияclose()проверять флаг труднее — вы уже вышли из блокаtry. Внутри блока — правильное место для проверки. PrintWriterс включённым автосбросом, обёртывающийSystem.out, вывел отчёт с выравниванием по столбцам, потому что каждыйprintfзаканчивался%n, что запускало сброс буфера. Он не обёрнут вtry-with-resources — закрытиеPrintWriter, декорирующегоSystem.out, закрыло бы самSystem.outи заглушило бы всё, что выводится после, поэтому в примере используется flush вместо этого.- В четвёртом блоке создавался writer, чей
writeвсегда бросает исключение, и на него указывалсяPrintWriter.printlnвернулся нормально — никакого исключения для перехвата.checkError()вернулtrue, и это был единственный способ узнать, что запись не удалась. Таков компромисс дизайна «проглатывай и флагируй»: удобен для быстрого кода, опасен если не проверять.
Что дальше
PrintWriter — это символьно-ориентированный файловый writer. Его аналог, Java PrintStream, — байт-ориентированный, обеспечивающий работу System.out и System.err — тот же API, другой предок, и причина, по которой терминальный вывод работает для любого символа, помещающегося в кодировку платформы по умолчанию.