W3docs

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, чей базовый поток был закрыт.

java— editable, runs on the server

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

  • CSV-файл получился именно таким, как задала строка формата printf. %.2f округлил цену до двух знаков после запятой; %-10s выровнял по левому краю в колонке шириной 10 символов; \n (а не %n) сделал символ переноса строки переносимым. Строки формата — главная причина выбирать PrintWriter вместо обычного Writer.
  • Первый блок try-with-resources свёл стек из четырёх слоёв — PathFileOutputStreamOutputStreamWriter(UTF-8)BufferedWriterPrintWriter — к одному вызову конструктора. Конструктор (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, другой предок, и причина, по которой терминальный вывод работает для любого символа, помещающегося в кодировку платформы по умолчанию.

Практика

Практика
Вы записываете файл из 10 000 строк с помощью `PrintWriter` и никогда не вызываете `checkError()`. Три строки в середине не удалось записать из-за заполнения диска. Как выглядит итоговый файл и что сообщает программа?
Вы записываете файл из 10 000 строк с помощью `PrintWriter` и никогда не вызываете `checkError()`. Три строки в середине не удалось записать из-за заполнения диска. Как выглядит итоговый файл и что сообщает программа?
Was this page helpful?