Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 1. Классический Java I/O (java.io)
Правильная работа с текстовыми файлами: InputStreamReader и OutputStreamWriter
Классы
Эта неопределенность делает
InputStreamReader: декодирование байтов в символы
Конструктор принимает
Путь данных через память JVM
Путь данных детально:
Внутреннее устройство
Механика декодирования UTF-8
UTF-8 — кодировка переменной длины. Декодер должен обрабатывать мультибайтовые последовательности, разбитые между блоками чтения:
Это требует внутреннего состояния в
OutputStreamWriter: кодирование символов в байты
Путь данных через память JVM
Путь данных детально:
Кодирование и размер выходных данных
Строка "Hello, 世界! 🌍" содержит:
8 символов ASCII: 8 байт UTF-8
'世' (U+4E16): 3 байта UTF-8
'界' (U+754C): 3 байта UTF-8
'🌍' (U+1F30D): суррогатная пара, 4 байта UTF-8
Итого: 18 байт для 11 кодовых точек, 12
#Java #для_новичков #beginner #IO #NIO #InputStreamReader #OutputStreamWriter
Глава 1. Классический Java I/O (java.io)
Правильная работа с текстовыми файлами: InputStreamReader и OutputStreamWriter
Классы
FileReader и FileWriter — удобные обертки, но содержат фундаментальный архитектурный дефект: они используют кодировку платформы по умолчанию, определяемую системным свойством file.encoding. Это свойство зависит от операционной системы, locale пользователя и параметров запуска JVM.// Антипаттерн: неявная кодировка платформы
try (FileReader reader = new FileReader("text.txt")) {
// На Windows может использоваться windows-1251
// На Linux — UTF-8
// На macOS — UTF-8
// Результат непредсказуем при переносе между системами
}
Эта неопределенность делает
FileReader/FileWriter непригодными для переносимого кода. Файл, записанный на одной системе, может быть искажен при чтении на другой. Современная практика рекомендует явное указание кодировки UTF-8 для всех текстовых операций.InputStreamReader: декодирование байтов в символы
InputStreamReader — мост от байтовых потоков к символьным. Он читает байты из InputStream и декодирует их в символы char с использованием указанной кодировки.public InputStreamReader(InputStream in, Charset cs)
Конструктор принимает
InputStream — источник сырых байтов, и Charset — правило декодирования. StandardCharsets.UTF_8 — предопределенная константа, исключающая риск неподдерживаемой кодировки.Путь данных через память JVM
try (InputStreamReader reader = new InputStreamReader(
new FileInputStream("text.txt"), StandardCharsets.UTF_8)) {
int charValue;
while ((charValue = reader.read()) != -1) {
System.out.print((char) charValue);
}
}
Путь данных детально:
[Файл на диске: байты UTF-8]
-> [Page Cache ОС: байты файла]
-> [FileInputStream.read(byte[]) — системный вызов]
-> [Нативный буфер userspace]
-> [JNI: копирование в byte[] внутри InputStreamReader]
-> [InputStreamReader: накопление байт во внутреннем ByteBuffer]
-> [CharsetDecoder UTF-8: конечный автомат декодирования]
-> [CharBuffer: накопление декодированных символов]
-> [read(): возврат char как int]
-> [System.out.print(char): кодирование в байты платформы]
Внутреннее устройство
InputStreamReader из OpenJDK:// Упрощенная структура
public class InputStreamReader extends Reader {
private final StreamDecoder sd; // Делегат декодирования
// StreamDecoder содержит:
// - ByteBuffer: входные байты из потока
// - CharBuffer: выходные символы
// - CharsetDecoder: конечный автомат кодировки
}
StreamDecoder — внутренний класс, выполняющий реальную работу. Он управляет ByteBuffer для входных байт и CharBuffer для выходных символов. Буферы создаются при конструировании и живут до закрытия потока.Механика декодирования UTF-8
UTF-8 — кодировка переменной длины. Декодер должен обрабатывать мультибайтовые последовательности, разбитые между блоками чтения:
// Сценарий: 3-байтовый символ '世' (U+4E16, E4 B8 96) разбит между чтениями
// Первое чтение: получены байты [..., E4, B8] — неполная последовательность
// StreamDecoder сохраняет E4 B8 во внутреннем ByteBuffer, возвращает 0 символов
// Второе чтение: получен байт [96, ...] — завершение последовательности
// StreamDecoder комбинирует E4 B8 96, декодирует в U+4E16, возвращает '世'
Это требует внутреннего состояния в
StreamDecoder: неполные байтовые последовательности сохраняются между вызовами read(). Буфер для этих остатков — часть ByteBuffer в heap.OutputStreamWriter: кодирование символов в байты
OutputStreamWriter — зеркальный мост: принимает символы char или String, кодирует их в байты согласно указанной кодировке, и записывает в OutputStream.public OutputStreamWriter(OutputStream out, Charset cs)
Путь данных через память JVM
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream("output.txt"), StandardCharsets.UTF_8)) {
writer.write("Hello, 世界! 🌍");
}
Путь данных детально:
[Java-код: String "Hello, 世界! 🌍"]
-> [String.toCharArray() или прямой доступ к внутреннему char[]]
-> [OutputStreamWriter.write(char[], off, len)]
-> [StreamEncoder: кодирование char -> байты]
-> [CharsetEncoder UTF-8: конечный автомат]
-> [ByteBuffer: накопление закодированных байт]
-> [FileOutputStream.write(byte[]): системный вызов]
-> [Нативный буфер userspace]
-> [Ядро ОС: syscall write]
-> [Page Cache: dirty pages]
-> [Фоновая запись на диск]
StreamEncoder — внутренний аналог StreamDecoder. Он управляет CharBuffer для входных символов и ByteBuffer для выходных байт. При вызове write(String) символы сначала копируются в CharBuffer, затем кодер преобразует их в байты UTF-8.Кодирование и размер выходных данных
Строка "Hello, 世界! 🌍" содержит:
8 символов ASCII: 8 байт UTF-8
'世' (U+4E16): 3 байта UTF-8
'界' (U+754C): 3 байта UTF-8
'🌍' (U+1F30D): суррогатная пара, 4 байта UTF-8
Итого: 18 байт для 11 кодовых точек, 12
char в JavaStreamEncoder должен обрабатывать суррогатные пары корректно: старший суррогат без младшего не является валидным символом и должен либо ждать дополнительных данных, либо генерировать замену (replacement character U+FFFD).#Java #для_новичков #beginner #IO #NIO #InputStreamReader #OutputStreamWriter
👍3