Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 1. Классический Java I/O (java.io)
BufferedReader – построчное чтение. Путь данных и управление памятью
Ключевое архитектурное решение — разделение между транспортом и семантикой.
Внутреннее устройство
Поле
Механика readLine()
Ключевые особенности:
Универсальность разделителей.
Разделитель не включается в результат. Возвращенная
Признак конца потока.
Путь данных в памяти
Жизненный цикл объектов в heap
При каждом вызове readLine() создаются объекты:
Внутренний массив
Это создает дополнительное давление на GC для файлов с очень длинными строками.
Роль garbage collector
Сценарий: чтение файла с миллионом строк
Память в процессе:
Если вызывающий код не сохраняет
Проблема: сохранение всех строк
Здесь все
Решение — потоковая обработка без накопления:
#Java #для_новичков #beginner #IO #NIO #BufferedReader
Глава 1. Классический Java I/O (java.io)
BufferedReader – построчное чтение. Путь данных и управление памятью
BufferedReader — декоратор над Reader, добавляющий буферизацию символьного ввода и специализированные методы для построчного чтения. В отличие от базового Reader, который оперирует массивами char[], BufferedReader предоставляет readLine() — метод, возвращающий строку текста, завершающуюся символом перевода строки.Ключевое архитектурное решение — разделение между транспортом и семантикой.
InputStreamReader декодирует байты в символы. BufferedReader накапливает символы в буфере и распознает логические строки, абстрагируя приложение от деталей разделителей строк (\n, \r, \r\n).Внутреннее устройство
// Упрощенная структура из OpenJDK
public class BufferedReader extends Reader {
// Буфер символов в heap JVM
private char[] cb;
// Количество символов в буфере
private int nChars;
// Текущая позиция чтения
private int nextChar;
// Маркер для mark/reset
private int markedChar = UNMARKED;
private int readAheadLimit = 0;
// Признак пропуска \n после \r
private boolean skipLF = false;
// Конструкторы
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize); // 8192 по умолчанию
}
public BufferedReader(Reader in, int sz) {
super(in);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
cb = new char[sz]; // Выделение в heap
}
}
Поле
cb — массив char[] в heap, служащий кольцевым буфером для накопления символов из underlying Reader. Размер по умолчанию 8192 символов, что соответствует 16 KB памяти heap (каждый char — 2 байта).Механика readLine()
readLine() выполняет последовательный поиск символов перевода строки в буфере, накапливая символы до разделителя во временный StringBuilder, и создает String при нахождении \n или \r\n:// Упрощенная логика readLine()
public String readLine() throws IOException {
StringBuilder sb = new StringBuilder(defaultExpectedLineLength);
while (true) {
if (nextChar >= nChars) {
fill(); // Заполнение буфера из underlying Reader
if (nextChar >= nChars) {
// Конец потока
return sb.length() > 0 ? sb.toString() : null;
}
}
char c = cb[nextChar++];
if (c == '\n') {
if (skipLF) {
skipLF = false;
continue; // Пропуск \n после \r
}
return sb.toString(); // Строка завершена
}
if (c == '\r') {
skipLF = true;
return sb.toString(); // Строка завершена, \n будет пропущен
}
sb.append(c); // Накопление символов
}
}
Ключевые особенности:
Универсальность разделителей.
\n (Unix), \r (старый Mac), \r\n (Windows) распознаются корректно.Разделитель не включается в результат. Возвращенная
String не содержит \n или \r.Признак конца потока.
null возвращается, когда достигнут EOF и StringBuilder пуст.Путь данных в памяти
[Диск: файл в байтах UTF-8]
-> [Page Cache ОС]
-> [FileInputStream: системный вызов read()]
-> [InputStreamReader: декодирование байт -> char]
-> [BufferedReader.fill(): копирование char[] в cb[8192]]
-> [BufferedReader.readLine(): поиск \n в cb]
-> [StringBuilder: накопление символов строки]
-> [StringBuilder.toString(): создание String в heap]
-> [Возврат String]
Жизненный цикл объектов в heap
При каждом вызове readLine() создаются объекты:
StringBuilder — временный накопитель символов. Создается при каждом вызове readLine(), живет до возврата String. Собирается GC немедленно.String — результат. Создается через StringBuilder.toString(). В современных JDK (9+) String хранит byte[] вместо char[], с компактным представлением для латиницы (1 байт на символ) и UTF-16 для остального.Внутренний массив
StringBuilder — char[], расширяющийся при необходимости. При превышении емкости создается новый массив большего размера с System.arraycopy.// Длинная строка превышает начальную емкость StringBuilder
StringBuilder sb = new StringBuilder(80); // Начальный массив char[80]
// Строка длиной 200 символов
// sb расширяется: char[80] -> char[160] -> char[320] -> ...
// Старые массивы становятся мусором
Это создает дополнительное давление на GC для файлов с очень длинными строками.
Роль garbage collector
Сценарий: чтение файла с миллионом строк
public long countLines(String path) throws IOException {
long lineCount = 0;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
lineCount++;
// line — новая String в heap
// StringBuilder и его внутренний char[] — мусор
}
} // reader закрыт, cb освобожден
return lineCount;
}Память в процессе:
cb[8192] — буфер BufferedReader, живет до закрытияStringBuilder + внутренний char[] — при каждом вызове readLine()String — при каждом вызове readLine(), возвращается вызывающемуЕсли вызывающий код не сохраняет
String, она становится мусором после итерации. Для файла с 1 000 000 строк создается 1 000 000 объектов String + 1 000 000 StringBuilder + несколько расширенных char[] для длинных строк. Все они короткоживущие и собираются minor GC в young generation.Проблема: сохранение всех строк
// Антипаттерн: удержание всех строк в памяти
public List<String> readAllLines(String path) throws IOException {
List<String> lines = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
lines.add(line); // Все String удерживаются списком
}
}
return lines; // Возврат: все строки в heap
}
Здесь все
String удерживаются списком lines. Для файла 100MB с ~1 000 000 строк это ~100MB в String + оверхед ArrayList. GC не может собрать эти String, пока список достижим. Это приводит к увеличению old generation и потенциальному OutOfMemoryError.Решение — потоковая обработка без накопления:
// Правильно: потоковая обработка без хранения всех строк
public void processLines(String path, Consumer<String> processor) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
processor.accept(line);
// line становится недостижимой после итерации, если processor не сохраняет
}
}
}
#Java #для_новичков #beginner #IO #NIO #BufferedReader
👍4
Проблема: сохранение всех строк
Здесь все
Решение — потоковая обработка без накопления:
Практический пример: чтение файла построчно с подсчетом
Анализ памяти:
Все
Оптимизация: избежание хранения самой длинной строки
Здесь ни одна
Проблема: очень длинные строки
Если файл содержит строку, превышающую размер
Расширение
Для предотвращения можно указать начальную емкость:
Однако
Альтернатива: Files.lines() и Stream API
Java 8 предоставляет
Внутри
#Java #для_новичков #beginner #IO #NIO #BufferedReader
// Антипаттерн: удержание всех строк в памяти
public List<String> readAllLines(String path) throws IOException {
List<String> lines = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
lines.add(line); // Все String удерживаются списком
}
}
return lines; // Возврат: все строки в heap
}
Здесь все
String удерживаются списком lines. Для файла 100MB с ~1 000 000 строк это ~100MB в String + оверхед ArrayList. GC не может собрать эти String, пока список достижим. Это приводит к увеличению old generation и потенциальному OutOfMemoryError.Решение — потоковая обработка без накопления:
// Правильно: потоковая обработка без хранения всех строк
public void processLines(String path, Consumer<String> processor) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
processor.accept(line);
// line становится недостижимой после итерации, если processor не сохраняет
}
}
}
Практический пример: чтение файла построчно с подсчетом
import java.io.*;
import java.nio.charset.StandardCharsets;
public class LineCounter {
public LineStats analyzeFile(String path) throws IOException {
long lineCount = 0;
long totalChars = 0;
long maxLineLength = 0;
String longestLine = null;
// BufferedReader с underlying InputStreamReader и FileInputStream
// cb[8192] создается в heap при конструировании
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
lineCount++;
long lineLength = line.length();
totalChars += lineLength;
if (lineLength > maxLineLength) {
maxLineLength = lineLength;
longestLine = line; // Сохранение ссылки — String остается в heap!
}
}
} // close() освобождает cb, но longestLine удерживается
return new LineStats(lineCount, totalChars, maxLineLength, longestLine);
}
public record LineStats(long lineCount, long totalChars,
long maxLineLength, String longestLine) {}
}
Анализ памяти:
cb[8192] — освобожден при закрытии, GC собираетВсе
String из readLine() — собраны GC, кроме longestLinelongestLine — удерживается возвращаемым LineStats, живет до потери ссылкиОптимизация: избежание хранения самой длинной строки
// Если нужна только длина, не хранить строку
public LineStats analyzeFileOptimized(String path) throws IOException {
long lineCount = 0;
long totalChars = 0;
long maxLineLength = 0;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
lineCount++;
long lineLength = line.length();
totalChars += lineLength;
maxLineLength = Math.max(maxLineLength, lineLength);
// line не сохраняется — собирается GC
}
}
return new LineStats(lineCount, totalChars, maxLineLength, null);
}
Здесь ни одна
String не удерживается. Все собираются minor GC, память остается минимальной даже для гигабайтных файлов.Проблема: очень длинные строки
Если файл содержит строку, превышающую размер
cb (8192 символов), readLine() выполняет несколько итераций fill():// Строка длиной 100 000 символов без \n
// readLine() выполняет ~13 вызовов fill(), накапливая в StringBuilder
StringBuilder sb = new StringBuilder(80); // Начальный массив char[80]
// При достижении 80: расширение до 160, копирование
// При достижении 160: расширение до 320, копирование
// ...
// Итоговый массив char[131072] для 100000 символов
Расширение
StringBuilder создает промежуточные массивы char[], которые становятся мусором. Для предотвращения можно указать начальную емкость:
// Если известна ожидаемая длина строки
StringBuilder sb = new StringBuilder(expectedLineLength);
Однако
readLine() не позволяет контролировать StringBuilder — он создается внутри метода с фиксированной начальной емкостью.Альтернатива: Files.lines() и Stream API
Java 8 предоставляет
Files.lines() — потоковый API для построчного чтения:// Потоковое чтение без явного BufferedReader
public long countLinesStream(String path) throws IOException {
try (Stream<String> lines = Files.lines(Path.of(path), StandardCharsets.UTF_8)) {
return lines.count();
}
}
Внутри
Files.lines() создает BufferedReader, но инкапсулирует его в Stream. Преимущество — ленивое выполнение и встроенные методы обработки. Недостаток — Stream требует закрытия, иначе файловый дескриптор остается открытым.#Java #для_новичков #beginner #IO #NIO #BufferedReader
👍5