Java for Beginner
870 subscribers
1.01K photos
275 videos
14 files
1.69K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)

Глава 1. Классический Java I/O (java.io)

BufferedWriter – запись строк. Буферизация и путь данных в памяти JVM

BufferedWriter — декоратор над Writer, добавляющий внутреннюю буферизацию символьного вывода. Он решает проблему, зеркальную BufferedReader: накладные расходы системных вызовов при частых мелких операциях записи. Вместо немедленной отправки каждого символа или строки в underlying Writer, BufferedWriter накапливает данные в массиве char[] в heap и сбрасывает их пачками.

Ключевое отличие от BufferedOutputStream: BufferedWriter работает с символами char (16 бит), а не байтами. Это означает, что его внутренний буфер хранит char[], а преобразование в байты происходит позже — в underlying OutputStreamWriter, который кодирует символы в UTF-8 или другую кодировку.


Внутреннее устройство
// Упрощенная структура из OpenJDK
public class BufferedWriter extends Writer {
// Внутренний буфер символов в heap JVM
private char[] cb;

// Текущая позиция записи в буфере
private int nChars;

// Ссылка на underlying Writer
private Writer out;

// Разделитель строки для newLine()
private String lineSeparator;

// Конструкторы
public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize); // 8192 по умолчанию
}

public BufferedWriter(Writer out, int sz) {
super(out);
if (sz <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
this.out = out;
cb = new char[sz]; // Выделение в heap: 8192 * 2 = 16384 байт
nChars = 0;
lineSeparator = System.lineSeparator(); // Платформенный разделитель
}
}


Поле cb — массив char[] размером 8192 по умолчанию, что занимает 16 KB в heap. lineSeparator инициализируется значением System.lineSeparator(), которое равно \n на Unix/Linux/macOS и \r\n на Windows.
Механика write(String) и путь данных


Накопление в буфере
public void write(String s, int off, int len) throws IOException {
// Если строка не влезает в оставшийся буфер — сброс
if (len >= cb.length - nChars) {
flushBuffer(); // Сброс накопленного в underlying Writer
}

if (len >= cb.length) {
// Строка больше буфера — запись напрямую, минуя буфер
writeDirect(s, off, len);
} else {
// Копирование строки в буфер
s.getChars(off, off + len, cb, nChars); // Быстрое копирование из String
nChars += len;
}
}


Путь данных при write("Hello"):
[Java: bw.write("Hello")]
-> [BufferedWriter: len=5 < cb.length - nChars?]
-> [s.getChars(0, 5, cb, nChars)] // Копирование char[] из String в buf
-> [System.arraycopy: копирование 5 char в cb]
-> [nChars += 5]
-> [Возврат]

Никаких системных вызовов, никакого кодирования. Пять символов копируются в cb через System.arraycopy — нативный метод, оптимизированный JVM.


Сброс буфера: flushBuffer()
private void flushBuffer() throws IOException {
if (nChars > 0) {
out.write(cb, 0, nChars); // Запись в underlying Writer
nChars = 0; // Сброс позиции
}
}


Путь данных при заполнении буфера:
[BufferedWriter: flushBuffer()]
-> [OutputStreamWriter.write(cb, 0, nChars)]
-> [StreamEncoder: кодирование char[] -> байты UTF-8]
-> [ByteBuffer: накопление закодированных байт]
-> [FileOutputStream.write(byte[]): системный вызов]
-> [Нативный буфер userspace]
-> [Ядро ОС: syscall write]
-> [Page Cache: dirty pages]

Ключевой момент: BufferedWriter не знает о кодировке. Он накапливает char[], а кодирование в байты происходит в OutputStreamWriter. Это означает, что буфер BufferedWriter содержит символы, а буфер StreamEncoder — байты. Два уровня буферизации создают двойное копирование при сбросе.


Метод newLine(): платформенная независимость
public void newLine() throws IOException {
write(lineSeparator); // Запись \n или \r\n из поля lineSeparator
}


newLine()
записывает разделитель строки, определенный при конструировании. Это позволяет генерировать файлы с правильными разделителями для целевой платформы, независимо от платформы выполнения.
// На Unix: запись \n
// На Windows: запись \r\n
// Результат одинаков для кода, различается для файла



#Java #для_новичков #beginner #IO #NIO #BufferedWriter
👍4
Путь данных в памяти: детальный разбор

Сценарий: запись списка строк
public void writeLines(String path, List<String> lines) throws IOException {
// cb[8192] создается в heap при конструировании BufferedWriter
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(path), StandardCharsets.UTF_8))) {

for (String line : lines) {
writer.write(line); // Копирование в cb
writer.newLine(); // Копирование \n или \r\n в cb
// Системный вызов отсутствует, пока cb не заполнен
}

// Явный flush для гарантии сброса
writer.flush();

} // close() с неявным flush()
}

Память в процессе:
cb[8192] — буфер BufferedWriter, 16 KB heap
ByteBuffer внутри StreamEncoder — буфер для кодирования, ~24 KB heap
String из lines — удерживаются списком, не создаются заново
Никаких новых аллокаций в цикле, кроме возможных расширений ByteBuffer


Сценарий: буфер заполнен

// Допустим, cb заполнен на 8188 символов
// Запись строки "Hello" (5 символов + 1 \n = 6)

writer.write("Hello"); // 8188 + 5 = 8193 > 8192 — сброс!
writer.newLine(); // \n записывается в пустой cb


[write("Hello")]
-> [len=5 >= cb.length - nChars? 5 >= 4? да]
-> [flushBuffer()]
-> [out.write(cb, 0, 8188)] // Сброс 8188 символов в OutputStreamWriter
-> [StreamEncoder: кодирование 8188 char в байты UTF-8]
-> [FileOutputStream: системный вызов]
-> [nChars = 0]
-> [s.getChars(0, 5, cb, 0)] // "Hello" в cb[0..4]
-> [nChars = 5]

[newLine()]
-> [write("\n")] // или "\r\n"
-> [s.getChars(0, 1, cb, 5)] // \n в cb[5]
-> [nChars = 6]



Роль garbage collector

Жизненный цикл буферов
public void processAndWrite(String inputPath, String outputPath) throws IOException {
// Все объекты создаются в heap
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(inputPath), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(outputPath), StandardCharsets.UTF_8))) {

String line;
while ((line = reader.readLine()) != null) {
String processed = processLine(line); // Новая String в heap
writer.write(processed); // Копирование в cb
writer.newLine();
// processed становится мусором, если не сохранена
}

} // Все потоки закрыты, все буферы освобождены

// reader.cb, writer.cb, ByteBuffer'ы StreamEncoder — недостижимы
// GC собирает все при следующей minor collection
}


Анализ памяти:

reader.cb[8192] — 16 KB, освобожден при закрытии
writer.cb[8192] — 16 KB, освобожден при закрытии
ByteBuffer декодера — ~16 KB, освобожден
ByteBuffer кодера — ~24 KB, освобожден
line — собирается GC каждую итерацию
processed — собирается GC, если не сохранена
Общий пик памяти: ~72 KB на буферы + строки. Для больших файлов это незначительно.


Проблема: очень длинные строки

// Строка длиной 100 000 символов
String hugeLine = generateHugeLine();

// write() обнаруживает, что len >= cb.length
// Прямая запись, минуя буфер
writer.write(hugeLine);

При превышении размера буфера BufferedWriter не копирует строку в cb, а передает её напрямую в underlying Writer. Это экономит память (не дублирует строку в буфере), но лишает преимущества буферизации — системный вызов выполняется немедленно.

Практический пример: запись списка строк в файл

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class LineWriter {

public void writeLines(String path, List<String> lines) throws IOException {
// Путь данных:
// String -> char[] -> cb[BufferedWriter] -> ByteBuffer[StreamEncoder] -> FileOutputStream -> page cache -> диск

try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(path), StandardCharsets.UTF_8))) {

for (String line : lines) {
writer.write(line); // Копирование в cb
writer.newLine(); // Платформенный разделитель
}

// Явный flush перед закрытием
writer.flush();

} // close() с неявным flush()

// Все буферы в heap освобождены, GC собирает
}

// Альтернатива: запись с заголовком и подвалом
public void writeReport(String path, String header,
List<String> data, String footer) throws IOException {
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(path), StandardCharsets.UTF_8))) {

writer.write(header);
writer.newLine();
writer.newLine(); // Пустая строка

for (String line : data) {
writer.write(line);
writer.newLine();
}

writer.newLine(); // Пустая строка перед подвалом
writer.write(footer);
writer.newLine();

}
}
}


Сравнение с небуферизованной записью
// Антипаттерн: запись без буферизации
public void writeUnbuffered(String path, List<String> lines) throws IOException {
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(path), StandardCharsets.UTF_8)) {

for (String line : lines) {
writer.write(line); // Каждый символ = системный вызов!
writer.write('\n');
}
}
}


OutputStreamWriter
без BufferedWriter кодирует и записывает каждую порцию данных немедленно. Для строки "Hello" это означает: кодирование 5 символов в 5 байт UTF-8, системный вызов write(fd, buf, 5). Для 1 000 000 строк — 2 000 000 системных вызовов.

С BufferedWriter: накопление в cb, сброс при заполнении. Для средней строки 80 символов + 1 \n = 81 символ. Буфер 8192 вмещает ~100 строк. Системных вызовов: 1 000 000 / 100 = 10 000. Ускорение в 200 раз.


#Java #для_новичков #beginner #IO #NIO #BufferedWriter
👍4