Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 1. Классический Java I/O (java.io)
PrintWriter и PrintStream – форматированный вывод. Путь данных и управление памятью
Внутреннее устройство PrintWriter
При конструировании
Рекомендуется оборачивать в
Методы форматированного вывода
print() и println()
printf() и format()
Путь данных в памяти при printf()
Путь данных детально:
Объекты в heap при одном вызове
#Java #для_новичков #beginner #IO #NIO #PrintWriter #PrintStream
Глава 1. Классический Java I/O (java.io)
PrintWriter и PrintStream – форматированный вывод. Путь данных и управление памятью
PrintWriter и PrintStream — высокоуровневые обертки для форматированного вывода текста. Они предоставляют методы print(), println(), printf(), которые автоматически преобразуют примитивы, объекты и строки в текстовое представление и записывают в underlying поток. Ключевое отличие от базовых Writer/OutputStream: эти классы никогда не выбрасывают IOException. Вместо этого они устанавливают внутренний флаг ошибки, проверяемый методом checkError().PrintStream — байтовый поток, наследник OutputStream. Исторически использовался для System.out и System.err. PrintWriter — символьный поток, наследник Writer. Предпочтителен для нового кода, так как работает с символами и корректно обрабатывает Unicode.Внутреннее устройство PrintWriter
// Упрощенная структура из OpenJDK
public class PrintWriter extends Writer {
// Underlying Writer для фактической записи
private Writer out;
// Флаг autoFlush: сброс буфера после println()
private boolean autoFlush;
// Флаг ошибки: true, если произошла IOException
private boolean trouble = false;
// Конструкторы
public PrintWriter(Writer out) {
this(out, false); // autoFlush = false по умолчанию
}
public PrintWriter(Writer out, boolean autoFlush) {
super(out);
this.out = out;
this.autoFlush = autoFlush;
}
public PrintWriter(String fileName) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(fileName))), false);
}
}
При конструировании
new PrintWriter(new FileWriter(...)) создается цепочка оберток:PrintWriter
-> Writer (FileWriter)
-> FileWriter extends OutputStreamWriter
-> FileOutputStream (внутри OutputStreamWriter)
PrintWriter не имеет собственного буфера — он делегирует запись в underlying Writer. Если underlying Writer не буферизован (как FileWriter), каждый вызов print() инициирует немедленную запись. Рекомендуется оборачивать в
BufferedWriter:// Правильно: буферизация через BufferedWriter
new PrintWriter(new BufferedWriter(new FileWriter("file.txt")));
Методы форматированного вывода
print() и println()
public void print(String s) {
if (s == null) {
s = "null";
}
write(s); // Делегирование в underlying Writer
}
public void println(String s) {
print(s);
println(); // Запись разделителя строки
}
public void println() {
// Запись line.separator
write(System.lineSeparator());
if (autoFlush) {
flush(); // Сброс при autoFlush=true
}
}
println() добавляет платформенный разделитель строки и, при autoFlush=true, вызывает flush(). Это гарантирует немедленную видимость данных, но снижает производительность.printf() и format()
public PrintWriter printf(String format, Object... args) {
return format(format, args);
}
public PrintWriter format(String format, Object... args) {
try {
Formatter f = new Formatter(this);
f.format(format, args);
} catch (FormatterClosedException x) {
// Обработка
}
return this;
}printf() создает объект Formatter — сложный парсер форматных строк. Formatter анализирует строку формата, извлекает аргументы, выполняет преобразования и записывает результат в Appendable (в данном случае — PrintWriter).Путь данных в памяти при printf()
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("report.txt")))) {
pw.printf("Book: %s, Author: %s, Price: %.2f%n",
book.getTitle(), book.getAuthor(), book.getPrice());
}
Путь данных детально:
[Java: pw.printf("Book: %s...", title, author, price)]
-> [new Formatter(pw)] // Создание Formatter в heap
-> [Formatter.parse(format)] // Парсинг строки формата
-> [StringBuilder внутри Formatter: накопление результата]
-> [%s: title.toString()] // Вызов toString(), создание String
-> [%s: author.toString()] // Вызов toString(), создание String
-> [%.2f: String.format("%.2f", price)] // Создание String
-> [%n: замена на \n или \r\n]
-> [Formatter.flush(): запись StringBuilder в PrintWriter]
-> [PrintWriter.write(String)] // Делегирование в BufferedWriter
-> [BufferedWriter.write(char[], off, len)] // Накопление в cb
-> [OutputStreamWriter: кодирование char -> байты UTF-8]
-> [FileOutputStream: системный вызов при заполнении/flush]Объекты в heap при одном вызове
printf():Formatter — временный, собирается GC после вызоваStringBuilder внутри Formatter — временныйString от toString() аргументов — если аргументы уже String, новые не создаютсяString от форматирования чисел — временная#Java #для_новичков #beginner #IO #NIO #PrintWriter #PrintStream
👍4
Роль garbage collector
Жизненный цикл объектов при форматировании
Пик памяти:
Оптимизация: ручное форматирование для горячих путей
Этот подход переиспользует
autoFlush: семантика и производительность
При
Путь данных с
Для логирования в реальном времени
Практический пример: запись книг в CSV
Путь данных и память при записи CSV
Для книги с
Объекты в heap за одну итерацию:
Все временные, собираются minor GC. Для 1 000 000 книг — ~360MB временных объектов, но они не накапливаются.
#Java #для_новичков #beginner #IO #NIO #PrintWriter #PrintStream
Жизненный цикл объектов при форматировании
public void writeBooksCsv(String path, List<Book> books) throws IOException {
// Объекты, создаваемые при конструировании:
// - FileWriter (heap)
// - BufferedWriter (heap) с cb[8192] (heap, 16KB)
// - PrintWriter (heap)
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter(path)))) {
pw.println("Title,Author,Price,Year"); // Заголовок
for (Book book : books) {
// Каждая итерация:
// - Formatter (временный, внутри printf)
// - StringBuilder (временный, внутри Formatter)
// - String от Double.toString() или String.format() (временный)
// - char[] при записи в BufferedWriter (переиспользуется)
pw.printf("%s,%s,%.2f,%d%n",
escapeCsv(book.getTitle()),
escapeCsv(book.getAuthor()),
book.getPrice(),
book.getYear());
// Formatter и StringBuilder собираются GC
// String от escapeCsv() — если не сохранена, собирается
}
} // close() с flush()
// Все объекты недостижимы, GC собирает
}Пик памяти:
BufferedWriter.cb (16KB) + Formatter + StringBuilder + временные String. Для списка в 1 000 000 книг создается 1 000 000 объектов Formatter и StringBuilder — все короткоживущие, собираются minor GC.Оптимизация: ручное форматирование для горячих путей
// Антипаттерн для горячих путей: printf создает много временных объектов
pw.printf("%s,%s,%.2f,%d%n", title, author, price, year);
// Оптимизация: ручное форматирование через StringBuilder
// (но PrintWriter не предоставляет доступа к внутреннему буферу)
// Альтернатива: накопление в собственном StringBuilder, затем одна запись
StringBuilder sb = new StringBuilder(256);
for (Book book : books) {
sb.setLength(0); // Сброс без создания нового объекта
sb.append(escapeCsv(book.getTitle())).append(',')
.append(escapeCsv(book.getAuthor())).append(',')
.append(String.format("%.2f", book.getPrice())).append(',')
.append(book.getYear()).append('\n');
pw.write(sb.toString());
}
Этот подход переиспользует
StringBuilder, устраняя создание Formatter. Однако String.format("%.2f", ...) все еще создает временные объекты. Для полной оптимизации требуется ручное форматирование чисел.autoFlush: семантика и производительность
// autoFlush=true: flush() после каждого println()
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("log.txt")), true)) {
pw.println("Entry 1"); // Запись + flush()
pw.println("Entry 2"); // Запись + flush()
// Каждый flush() = сброс BufferedWriter -> OutputStreamWriter -> FileOutputStream
}
При
autoFlush=true каждый println() инициирует полный сброс цепочки: BufferedWriter.flushBuffer() -> OutputStreamWriter.flush() -> FileOutputStream.flush() -> системный вызов. Это гарантирует немедленную запись, но снижает производительность на порядки.Путь данных с
autoFlush=true:[println("Entry")]
-> [print("Entry")]
-> [BufferedWriter.write("Entry")] // Накопление в cb
-> [println()]
-> [write("\n")] // Накопление в cb
-> [flush()] // СБРОС!
-> [BufferedWriter.flushBuffer()]
-> [OutputStreamWriter.write(cb, 0, n)]
-> [StreamEncoder: кодирование]
-> [FileOutputStream: системный вызов]
-> [Page Cache]Для логирования в реальном времени
autoFlush=true оправдан. Для пакетной записи — катастрофичен.Практический пример: запись книг в CSV
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class CsvBookWriter {
public void writeBooks(String path, List<Book> books) throws IOException {
// Цепочка оберток с буферизацией
// PrintWriter -> BufferedWriter -> OutputStreamWriter -> FileOutputStream
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(path), StandardCharsets.UTF_8)))) {
// Заголовок CSV
pw.println("id,title,author,price,year,pages");
// Данные
for (Book book : books) {
pw.printf("%d,\"%s\",\"%s\",%.2f,%d,%d%n",
book.getId(),
escapeCsv(book.getTitle()),
escapeCsv(book.getAuthor()),
book.getPrice(),
book.getYear(),
book.getPages());
}
// Явный flush перед закрытием
pw.flush();
// Проверка ошибок
if (pw.checkError()) {
throw new IOException("Error occurred during writing");
}
} // close() с неявным flush()
}
// Экранирование спецсимволов CSV
private String escapeCsv(String value) {
if (value == null) return "";
return value.replace("\"", "\"\"");
}
public record Book(int id, String title, String author,
double price, int year, int pages) {}
}
Путь данных и память при записи CSV
Для книги с
title="The Great Gatsby", author="F. Scott Fitzgerald", price=10.99:[printf("%d,\"%s\",\"%s\",%.2f,%d,%d%n", ...)]
-> [Formatter создан в heap]
-> [StringBuilder внутри Formatter создан в heap]
-> [Форматирование: "1,\"The Great Gatsby\",\"F. Scott Fitzgerald\",10.99,1925,180\n"]
-> [toString() для title — уже String, новый объект не создан]
-> [String.format("%.2f", 10.99) — создание String "10.99" в heap]
-> [Formatter.flush(): запись в PrintWriter]
-> [PrintWriter.write(String)]
-> [BufferedWriter.write(char[], 0, len)]
-> [Накопление в cb[8192]]
-> [При заполнении cb: сброс в OutputStreamWriter]
-> [Кодирование UTF-8]
-> [FileOutputStream: системный вызов]
-> [Formatter и StringBuilder — недостижимы, GC собирает]Объекты в heap за одну итерацию:
Formatter — ~200 байтStringBuilder — ~40 байт + внутренний char[] — ~80 байтString от String.format("%.2f", ...) — ~40 байт + char[]/byte[]Все временные, собираются minor GC. Для 1 000 000 книг — ~360MB временных объектов, но они не накапливаются.
PrintWriter предпочтителен для всего текстового вывода благодаря явному контролю кодировки и корректной работе с Unicode.#Java #для_новичков #beginner #IO #NIO #PrintWriter #PrintStream
👍4