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)

Символьные потоки: Reader и Writer. Unicode, кодировки и путь данных в памяти

Байтовые потоки (InputStream/OutputStream) оперируют единицей данных в 8 бит — байтом. Они не интерпретируют содержимое: байт 0x41 может быть буквой 'A' в ASCII, частью многобайтовой последовательности UTF-8, или элементом бинарного протокола.

Символьные потоки (Reader/Writer) оперируют единицей в 16 бит — char Java, который представляет кодовую точку Unicode в диапазоне U+0000..U+FFFF. Это различие коренным образом меняет архитектуру, семантику и путь данных через память JVM.

Unicode — стандарт кодирования символов, охватывающий большинство письменных систем мира. Java приняла Unicode с самого начала: тип char — беззнаковое 16-битное целое, String — неизменяемая последовательность char. Однако Unicode эволюционировал: первоначальная спецификация Unicode 1.0 предполагала 16-битное пространство (65 536 символов), но оказалось недостаточным. Unicode 2.0 ввел дополнительные плоскости за пределами Basic Multilingual Plane (BMP), и для их представления потребовались суррогатные пары — две 16-битные char, кодирующие одну кодовую точку в диапазоне U+10000..U+10FFFF. Это означает, что один "символ" в человеческом понимании может занимать один или два char в Java.


Архитектура Reader и Writer

Reader — абстрактный класс для чтения символов. Его методы зеркалируют InputStream, но работают с char вместо byte:
public abstract class Reader implements Readable, Closeable {
// Чтение одного символа (char как int для EOF-различения)
public int read() throws IOException

// Чтение в массив char[]
public int read(char[] cbuf) throws IOException

// Чтение в массив с офсетом
public abstract int read(char[] cbuf, int off, int len) throws IOException

// Пропуск символов
public long skip(long n) throws IOException

// Готовность к чтению без блокировки
public boolean ready() throws IOException

// Поддержка mark/reset
public boolean markSupported()
public void mark(int readAheadLimit) throws IOException
public void reset() throws IOException

// Закрытие
public abstract void close() throws IOException
}


Writer
— абстрактный класс для записи символов:
public abstract class Writer implements Appendable, Closeable, Flushable {
// Запись одного символа
public void write(int c) throws IOException

// Запись массива char[]
public void write(char[] cbuf) throws IOException

// Запись части массива
public abstract void write(char[] cbuf, int off, int len) throws IOException

// Запись строки
public void write(String str) throws IOException

// Запись части строки
public void write(String str, int off, int len) throws IOException

// Добавление (append)
public Writer append(CharSequence csq) throws IOException

// Сброс
public abstract void flush() throws IOException

// Закрытие
public abstract void close() throws IOException
}


Ключевое отличие от байтовых потоков: методы read() и write(int) работают с char (16 бит), а не с byte (8 бит). Возвращаемое значение read()int в диапазоне 0..65535 для валидных символов и -1 для EOF. Это сохраняет контракт различения валидных данных от признака конца потока, но для 16-битных char вместо 8-битных byte.


Кодировки: мост между байтами и символами


Символьные потоки не существуют в вакууме. Файлы, сетевые соединения, базы данных хранят и передают байты. Преобразование между байтами и символами выполняется через кодировку (charset) — таблицу соответствия последовательностей байт кодовым точкам Unicode.

UTF-8: доминирующая кодировка
UTF-8 — переменная длина кодирования Unicode:
U+0000..U+007F: 1 байт (ASCII-совместимость)
U+0080..U+07FF: 2 байта
U+0800..U+FFFF: 3 байта (большинство BMP-символов)
U+10000..U+10FFFF: 4 байта (суррогатные пары)

Преимущества UTF-8: ASCII-совместимость, компактность для латинских текстов, самосинхронизация (можно определить границу символа в произвольной позиции). Недостаток: переменная длина усложняет индексацию по символам — str.charAt(n) — O(1), но str.codePointAt(n) требует проверки суррогатов.


UTF-16: внутреннее представление Java

Java использует UTF-16 для внутреннего представления String и char[]. Каждый char — 16 бит. Символы BMP (U+0000..U+FFFF) представляются одним char. Символы за пределами BMP — суррогатной парой: старший суррогат (U+D800..U+DBFF) + младший суррогат (U+DC00..U+DFFF).
// Пример: эмодзи U+1F600 (😀) — суррогатная пара в Java
String emoji = "😀";
System.out.println(emoji.length()); // 2 — два char
System.out.println(emoji.codePointAt(0)); // 128512 — U+1F600

// Итерация по кодовым точкам, а не char
for (int cp : emoji.codePoints().toArray()) {
System.out.println("Code point: U+" + Integer.toHexString(cp));
}

Это критично для символьных потоков: Reader.read() возвращает int с кодовой точкой, корректно обрабатывая суррогатные пары. read(char[]) заполняет массив char, где суррогатные пары занимают два элемента.


Путь данных в памяти: от байтов файла до char[] в heap

Чтение текстового файла: FileReader
try (FileReader reader = new FileReader("text.txt")) {
int charValue;
while ((charValue = reader.read()) != -1) {
processChar((char) charValue);
}
}


Путь данных при чтении:
[Диск: файл в байтах UTF-8]
-> [Page Cache ОС: байты файла]
-> [FileInputStream (underlying FileReader): чтение байтов]
-> [InputStreamReader: декодирование байт -> char]
-> [CharsetDecoder UTF-8: конечный автомат для мультибайтовых последовательностей]
-> [char[] буфер декодера в heap]
-> [FileReader.read(): возврат char как int]
-> [Java-код: processChar()]


FileReader
— конкретный подкласс InputStreamReader, который автоматически открывает FileInputStream и использует кодировку платформы по умолчанию. Это опасно: кодировка по умолчанию зависит от ОС и locale, что делает поведение непредсказуемым при переносе между системами.
// Антипаттерн: зависимость от кодировки платформы
FileReader reader = new FileReader("text.txt"); // Неявная кодировка!

// Правильно: явное указание кодировки
InputStreamReader reader = new InputStreamReader(
new FileInputStream("text.txt"), StandardCharsets.UTF_8);



Декодирование и память

InputStreamReader использует CharsetDecoder из java.nio.charset. Декодер поддерживает внутренний буфер байт для неполных мультибайтовых последовательностей (например, первые 2 байта 3-байтового UTF-8 символа в конце блока чтения) и буфер char для выходных символов.
// Упрощенная логика декодирования
ByteBuffer in = ByteBuffer.allocate(8192); // Входные байты из потока
CharBuffer out = CharBuffer.allocate(8192); // Выходные символы

// Чтение байт из FileInputStream
int bytesRead = fileInputStream.read(in.array());
in.position(0).limit(bytesRead);

// Декодирование
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
CoderResult result = decoder.decode(in, out, false);

// Извлечение символов
char[] chars = new char[out.position()];
out.flip().get(chars);

Буферы ByteBuffer и CharBuffer — объекты в heap. CharsetDecoder — тоже объект в heap. При каждом создании InputStreamReader выделяется память для этих структур. При массовом открытии текстовых файлов без закрытия это создает давление на GC.


#Java #для_новичков #beginner #IO #NIO #Reader #Writer
👍4
Путь данных при записи: от char[] в heap до байтов файла

Запись текстового файла: FileWriter
try (FileWriter writer = new FileWriter("output.txt")) {
writer.write("Hello, 世界! 🌍");
}

Путь данных при записи:
plain
[Java-код: String "Hello, 世界! 🌍"]
-> [String.getBytes() или прямая работа с char[]]
-> [FileWriter (наследник OutputStreamWriter)]
-> [CharsetEncoder UTF-8: кодирование char -> байты]
-> [ByteBuffer буфер кодера в heap]
-> [FileOutputStream (underlying): запись байт]
-> [Page Cache ОС: dirty pages]
-> [Фоновая запись на диск]


Кодирование UTF-8 из char требует:
ASCII (U+0000..U+007F): 1 байт на символ
BMP non-ASCII (U+0080..U+FFFF): 2-3 байта
Суррогатные пары: 4 байта

Строка "Hello, 世界! 🌍" в Java:

'H', 'e', 'l', 'l', 'o', ',', ' ', '!': 8 символов ASCII → 8 байт UTF-8
'世' (U+4E16): 3 байта UTF-8
'界' (U+754C): 3 байта UTF-8
'🌍' (U+1F30D): суррогатная пара, 4 байта UTF-8
Итого: 8 + 3 + 3 + 4 = 18 байт UTF-8 для 11 "символов" в человеческом понимании, 12 char в Java


Роль garbage collector

Жизненный цикл объектов кодирования
public void processTextFiles(List<String> paths) throws IOException {
for (String path : paths) {
// Каждая итерация создает:
// - FileInputStream (heap)
// - InputStreamReader (heap)
// - CharsetDecoder (heap)
// - ByteBuffer/CharBuffer внутренние (heap)
// - char[] для чтения (heap)

try (InputStreamReader reader = new InputStreamReader(
new FileInputStream(path), StandardCharsets.UTF_8)) {

char[] buffer = new char[4096];
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
process(buffer, charsRead);
}

} // close() освобождает ресурсы, объекты становятся недостижимыми

// GC собирает все созданные объекты при следующей minor collection
}
}

При обработке тысяч файлов этот код создает тысячи временных объектов. Они короткоживущие и эффективно собираются minor GC, но при высокой частоте могут вызвать частые паузы young generation.


Оптимизация: переиспользование Reader/Writer

Для потоковой обработки текста из одного источника переиспользование буферов снижает нагрузку на GC:
// Переиспользуемый буфер
private static final ThreadLocal<char[]> CHAR_BUFFER =
ThreadLocal.withInitial(() -> new char[8192]);

public void processStream(InputStream in) throws IOException {
char[] buffer = CHAR_BUFFER.get();

try (InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
process(buffer, charsRead);
}
}
// reader и его декодер собираются GC
// buffer остается в ThreadLocal, не подлежит GC
}


Проблема: большие строки и heap
// Антипаттерн: чтение всего файла в одну строку
String content = new String(Files.readAllBytes("huge.txt"), StandardCharsets.UTF_8);


Files.readAllBytes
возвращает byte[] размером с файл. new String(...) создает char[] размером с количество символов (для ASCII — тот же размер, для UTF-8 с мультибайтом — меньше). Для файла 100MB это 100MB в byte[] + ~100MB в char[] = 200MB heap на один вызов. Если файл содержит преимущественно ASCII, byte[] и char[] примерно равны. Если содержит много 3-байтовых UTF-8 символов, char[] будет в 3 раза меньше byte[].


BufferedReader и BufferedWriter: буферизация символьных потоков

Аналогично байтовым потокам, символьные потоки имеют буферизованные обертки:
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("text.txt"), StandardCharsets.UTF_8), 8192)) {

String line;
while ((line = br.readLine()) != null) {
processLine(line);
}
}

BufferedReader содержит char[] cb в heap — буфер символов. readLine() накапливает символы до \n или \r\n, создавая новую String при каждом вызове. Эти строки — долгоживущие объекты, которые GC не собирает до потери ссылки.
java
// Путь данных readLine():
// 1. Чтение блока символов в cb[8192] из underlying Reader
// 2. Поиск \n в cb
// 3. new String(cb, start, end - start) — создание String в heap
// 4. Возврат String
// 5. Следующий вызов: повторение

Каждая String — отдельный объект в heap с собственным char[] (или byte[] в современных JDK с компакт-строками). При чтении файла 100MB по строкам в 100 символов создается ~1 000 000 объектов String. Это создает массивное давление на GC.


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