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)

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 для остального.

Внутренний массив StringBuilderchar[], расширяющийся при необходимости. При превышении емкости создается новый массив большего размера с 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
Проблема: сохранение всех строк
// Антипаттерн: удержание всех строк в памяти
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, кроме longestLine
longestLine — удерживается возвращаемым 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