Классы символьных потоков в Java: Reader и Writer
Файл hello.txt сохранён в UTF-8 и содержит одно слово «Привет». Программа на Java 17 под русской Windows читает его через new FileReader("hello.txt") и печатает Привет. Ошибки в коде нет: FileReader без явной кодировки взял системную — windows-1251. Как этого избежать и какой класс символьных потоков для чего нужен, разберём ниже.
Символьные потоки в Java — это классы пакета java.io, которые читают и записывают текст символами char (16-битными единицами UTF-16), а не отдельными байтами. Все символьные потоки чтения наследуются от абстрактного класса Reader, все потоки записи — от Writer. В файле или в сети всегда лежат байты, поэтому символьный поток, связанный с файлом, переводит байты в символы и обратно по кодировке (Charset). Общее деление потоков на байтовые и символьные описано в уроке Потоки данных в Java.
1. Класс Reader
Reader — абстрактный класс, от которого наследуются все символьные потоки ввода в Java. Его методы read() возвращают очередной символ как int от 0 до 65535 или -1, когда поток закончился. При ошибке ввода-вывода методы класса выбрасывают IOException.
Методы класса Reader
| Метод | Что делает | Граничный случай |
|---|---|---|
int read() | Читает один символ и возвращает его код как int от 0 до 65535 | В конце потока возвращает -1 |
int read(char[] buffer) | Читает до buffer.length символов в массив, возвращает количество реально прочитанных | Может прочитать меньше, чем размер массива; в конце потока — -1 |
int read(char[] buffer, int offset, int length) | Читает до length символов и кладёт их в массив, начиная с buffer[offset] | IndexOutOfBoundsException, если offset/length выходят за границы массива |
long skip(long n) | Пропускает до n символов | Возвращает количество реально пропущенных символов |
boolean ready() | Сообщает, можно ли прочитать символ без блокировки | false не означает конец потока — конец определяют только по -1 |
long transferTo(Writer out) (Java 10) | Копирует все оставшиеся символы в Writer | Возвращает количество скопированных символов |
void close() | Закрывает поток и освобождает ресурсы ОС | Последующие вызовы read() выбросят IOException: Stream closed |
С Java 11 есть фабричный метод Reader.nullReader() — пустой поток, который сразу возвращает -1; его удобно подставлять в тестах.
2. Класс Writer
Writer — абстрактный класс, от которого наследуются все символьные потоки вывода. Он определяет методы write() и append() для записи символов, массивов и строк, flush() для выталкивания буферизованных данных и close() для закрытия приёмника. При ошибке методы выбрасывают IOException.
Методы класса Writer
| Метод | Что делает | Граничный случай |
|---|---|---|
void write(int c) | Записывает один символ | Записываются только младшие 16 бит числа |
void write(char[] buffer) | Записывает весь массив символов | NullPointerException, если массив равен null |
void write(char[] buffer, int offset, int length) | Записывает length символов массива, начиная с позиции offset | IndexOutOfBoundsException при выходе за границы |
void write(String str) | Записывает строку | NullPointerException для null |
void write(String str, int offset, int length) | Записывает подстроку длиной length, начиная с позиции offset | IndexOutOfBoundsException при выходе за границы |
Writer append(CharSequence csq) | То же, что write(), но возвращает этот же Writer, поэтому вызовы можно объединять в цепочку: w.append("a").append('b') | Для null записывает строку "null", а не бросает исключение |
void flush() | Выталкивает накопленные в буферах символы в приёмник | Нужен не только BufferedWriter: даже FileWriter копит байты во внутреннем буфере кодировщика, и без flush()/close() они не попадут в файл |
void close() | Сбрасывает буфер (flush()) и закрывает поток | Последующие вызовы write() выбросят IOException |
Аналогично Reader, с Java 11 есть Writer.nullWriter() — поток, который отбрасывает всё записанное.
3. FileReader и FileWriter
FileReader читает символы из текстового файла, а FileWriter записывает символы в файл. В конструктор передают путь в виде строки или объект File (о нём — в уроке Класс File). Внутри это наследники InputStreamReader и OutputStreamWriter: они открывают файловый байтовый поток и переводят байты в символы по кодировке.
Конструкторы FileReader и FileWriter
| Конструктор | Что делает | Если что-то не так |
|---|---|---|
FileReader(String fileName), FileReader(File file) | Открывает файл на чтение в кодировке по умолчанию | FileNotFoundException, если файла нет или это каталог |
FileReader(String fileName, Charset charset) (Java 11) | То же, но с явной кодировкой | Неверные для кодировки байты заменяются символом � |
FileWriter(String fileName), FileWriter(File file) | Создаёт файл или обнуляет существующий | FileNotFoundException (подкласс IOException), если путь — каталог, файл только для чтения или нет родительского каталога |
FileWriter(String fileName, boolean append) | При append = true дописывает в конец файла, а не перезаписывает его | Если файла нет, он будет создан |
FileWriter(String fileName, Charset charset), FileWriter(String, Charset, boolean append) (Java 11) | Запись с явной кодировкой | Символы, которых нет в кодировке, записываются как ? |
До Java 11 у FileReader и FileWriter не было параметра Charset, поэтому для текста в нужной кодировке приходилось строить цепочку через InputStreamReader/OutputStreamWriter (см. следующий раздел).
Пример 1. Запись и чтение файла через FileWriter и FileReader
Потоки закрываются автоматически конструкцией try-with-resources. Запись и чтение идут в двух отдельных блоках: к моменту чтения FileWriter уже закрыт, и все символы гарантированно в файле.
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class FileWriterReaderExample {
public static void main(String[] args) {
File file = new File("fileWrite.txt");
try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) {
writer.write("Learn\nJava!\n");
} catch (IOException e) {
System.out.println("Ошибка записи: " + e.getMessage());
}
try (FileReader reader = new FileReader(file, StandardCharsets.UTF_8)) {
char[] buffer = new char[50];
int total = 0;
int count;
while ((count = reader.read(buffer)) != -1) { // читаем до конца файла
System.out.print(new String(buffer, 0, count));
total += count;
}
System.out.println("Прочитано символов: " + total);
} catch (IOException e) {
System.out.println("Ошибка чтения: " + e.getMessage());
}
}
}
// Вывод:
// Learn
// Java!
// Прочитано символов: 12 Символов 12: пять в слове Learn, пять в Java! и два перевода строки \n. Если запустить только вторую половину программы без файла, конструктор FileReader выбросит FileNotFoundException, и программа выведет Ошибка чтения: fileWrite.txt (...) с системным текстом причины.
4. InputStreamReader и OutputStreamWriter
InputStreamReader и OutputStreamWriter — мост между байтовыми и символьными потоками. InputStreamReader оборачивает любой InputStream и декодирует байты в символы, OutputStreamWriter оборачивает OutputStream и кодирует символы в байты. Кодировку задают вторым параметром конструктора — обычно StandardCharsets.UTF_8.
new InputStreamReader(InputStream in) // кодировка по умолчанию
new InputStreamReader(InputStream in, Charset cs) // явная кодировка
new InputStreamReader(InputStream in, String charsetName) // UnsupportedEncodingException при неизвестном имени
new OutputStreamWriter(OutputStream out)
new OutputStreamWriter(OutputStream out, Charset cs)
new OutputStreamWriter(OutputStream out, String charsetName) Метод getEncoding() возвращает историческое имя кодировки, с которой работает поток, например UTF8 или Cp1251. Эти классы нужны везде, где источник отдаёт байты, а работать хочется с текстом: файл (FileInputStream), сетевое соединение, консоль System.in:
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
String name = console.readLine(); // строка, введённая с клавиатуры Подробнее о стандартных потоках — в уроке Предопределенные потоки ввода-вывода. Как собрать из этих классов полную цепочку «файл → кодировка → буфер → форматирование» и почему кодировка по умолчанию зависела от ОС до Java 18 — в разделе Цепочка обёрток урока о потоках данных.
Что выведет этот код?
Программа записывает слово «Привет» в UTF-8, а затем читает его дважды: в кодировке windows-1251 и в UTF-8.
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class EncodingDemo {
public static void main(String[] args) throws IOException {
try (Writer out = new OutputStreamWriter(
new FileOutputStream("hello.txt"), StandardCharsets.UTF_8)) {
out.write("Привет");
}
try (BufferedReader in = new BufferedReader(new InputStreamReader(
new FileInputStream("hello.txt"), Charset.forName("windows-1251")))) {
String line = in.readLine();
System.out.println(line + " (" + line.length() + " символов)");
}
try (BufferedReader in = new BufferedReader(new InputStreamReader(
new FileInputStream("hello.txt"), StandardCharsets.UTF_8))) {
String line = in.readLine();
System.out.println(line + " (" + line.length() + " символов)");
}
}
}
// Вывод:
// Привет (12 символов)
// Привет (6 символов) В UTF-8 каждая буква кириллицы занимает 2 байта, поэтому в файле 12 байт. Кодировка windows-1251 однобайтовая: она превращает каждый байт в отдельный символ, и из 6 букв получается 12 «кракозябр» (П = байты D0 9F = Рџ). Ошибки и исключения нет — именно поэтому кодировку нужно указывать явно с обеих сторон: и при записи, и при чтении.
5. BufferedReader и BufferedWriter
BufferedReader и BufferedWriter — обёртки над другим символьным потоком с внутренним буфером на 8192 символа. BufferedReader читает данные в буфер крупными блоками и добавляет метод readLine() для чтения текста построчно; BufferedWriter накапливает символы в буфере, пишет их во вложенный поток блоками и добавляет метод newLine().
BufferedReader(Reader in)
BufferedReader(Reader in, int size) // size - размер буфера в символах
BufferedWriter(Writer out)
BufferedWriter(Writer out, int size) Первый параметр — поток, из которого читают или в который пишут (чаще всего FileReader/FileWriter или InputStreamReader/OutputStreamWriter), второй — размер буфера. Буфер сокращает число обращений к файлу: вместо системного вызова на каждый символ поток обращается к диску один раз на блок.
String readLine()возвращает очередную строку без символов конца строки (\n,\rили\r\n) иnull, когда файл закончился. Пустая строка в файле — это"", а неnull.void newLine()записывает разделитель строк текущей ОС:\r\nв Windows,\nв Linux и macOS.Stream<String> lines()(Java 8) возвращает строки файла как поток Stream API:long n = reader.lines().count();
Пример 2. Запись через BufferedWriter и чтение через BufferedReader
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class BufferedExample {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("buffered.txt", StandardCharsets.UTF_8))) {
writer.write("Привет, мир!");
writer.newLine();
writer.newLine(); // пустая строка
writer.write("Конец"); // последняя строка без перевода строки
} catch (IOException e) {
System.out.println("Ошибка записи: " + e.getMessage());
}
try (BufferedReader reader = new BufferedReader(
new FileReader("buffered.txt", StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) { // null - конец файла
System.out.println("[" + line + "] длина " + line.length());
}
} catch (IOException e) {
System.out.println("Ошибка чтения: " + e.getMessage());
}
}
}
// Вывод:
// [Привет, мир!] длина 12
// [] длина 0
// [Конец] длина 5 Квадратные скобки показывают, что readLine() отбросил символы конца строки, пустая строка пришла как "", а последняя строка без перевода строки всё равно прочитана. Закрывать достаточно внешнюю обёртку: BufferedWriter.close() сбросит буфер и закроет вложенный FileWriter — почему нельзя закрывать внутренний поток, разобрано в уроке Потоки данных.
Откуда берётся разделитель строк
newLine() и println() берут разделитель из System.lineSeparator(), поэтому файл, записанный в Windows, будет отличаться от файла из Linux на один байт в каждой строке. Если формат файла строго задан (например, CSV для другой системы), пишите разделитель явно: writer.write("\n"). Читать такие файлы можно на любой ОС — readLine() понимает все три варианта.
6. PrintWriter
PrintWriter — символьный поток вывода с удобными методами print(), println(), printf() и format(), которые превращают в текст значения любых типов. Он наследуется от Writer и, в отличие от остальных потоков, не выбрасывает IOException из методов записи: об ошибке сообщает метод checkError().
Конструкторы PrintWriter
| Конструктор | Что делает | Нюанс |
|---|---|---|
PrintWriter(String fileName), PrintWriter(File file) | Создаёт файл или перезаписывает существующий; кодировка по умолчанию | Выбрасывает FileNotFoundException, если файл нельзя создать или открыть |
PrintWriter(String fileName, Charset charset) (Java 10) | То же с явной кодировкой | Объявляет IOException |
PrintWriter(Writer out), PrintWriter(OutputStream out) | Пишет в существующий поток | Автоматического сброса нет: данные уходят дальше при flush(), close() или заполнении буфера |
PrintWriter(Writer out, boolean autoFlush), PrintWriter(OutputStream out, boolean autoFlush) | При autoFlush = true сбрасывает буфер после каждого вызова println(), printf() и format() | print() и write() не сбрасывают буфер даже с autoFlush |
Дописать в существующий файл конструкторами с именем файла нельзя — для этого оборачивают FileWriter в режиме добавления: new PrintWriter(new FileWriter("log.txt", true)).
Методы PrintWriter
print(x)— выводит значение любого типа в виде текста без перевода строки.println(x)— то же, затем разделитель строк ОС.printf(format, args...)иformat(format, args...)— одинаковые методы форматированного вывода по шаблону (%d,%s,%.2f,%n).write(...)— унаследованные отWriterметоды записи символов и строк без преобразования.boolean checkError()— сбрасывает буфер и возвращаетtrue, если при какой-либо записи произошла ошибка.
Пример 3. Запись в файл через PrintWriter
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
public class PrintWriterExample {
public static void main(String[] args) {
String s1 = "Привет мир!";
String s2 = "Hello World!";
try (PrintWriter printWriter = new PrintWriter("notes.txt", StandardCharsets.UTF_8)) {
printWriter.println(s1);
int i = 2;
printWriter.printf("Квадрат числа %d равен %d%n", i, i * i);
printWriter.write(s2);
printWriter.print("Конец");
} catch (IOException e) {
System.out.println("Не удалось открыть файл: " + e.getMessage());
return;
}
System.out.println("Запись в файл произведена");
}
}
// Вывод в консоль:
// Запись в файл произведена Сообщение печатается после блока try, то есть когда PrintWriter уже закрыт и данные сброшены в файл. Содержимое notes.txt:
Привет мир!
Квадрат числа 2 равен 4
Hello World!Конец Последняя строка склеилась: ни write(), ни print() не добавляют перевод строки — его ставят только println() и %n в шаблоне printf(). Блок catch здесь ловит только ошибку конструктора: сами вызовы println()/printf() исключений не выбрасывают.
Нюанс autoFlush
Если PrintWriter пишет в сокет или в другой процесс, создавайте его с autoFlush = true и отправляйте сообщения через println(). Строка, отправленная через print(), останется в буфере, и собеседник будет ждать ответа, который так и не уйдёт.
7. Подводные камни символьных потоков
Типичные ошибки с символьными потоками связаны с кодировкой по умолчанию, перезаписью файла вместо дописывания, буферизацией и тем, что PrintWriter молчит при сбоях. Все они компилируются без предупреждений и проявляются только на конкретных данных или конкретной ОС.
| Ошибка | Что происходит | Как правильно |
|---|---|---|
new FileReader("a.txt") без Charset на Java 17 и ниже | Файл в UTF-8 под русской Windows читается как Привет | new FileReader("a.txt", StandardCharsets.UTF_8) (Java 11+) или InputStreamReader с Charset |
new FileWriter("log.txt") для журнала | При каждом запуске файл обнуляется, старые записи пропадают | new FileWriter("log.txt", true) |
while (!(line = reader.readLine()).isEmpty()) | Чтение обрывается на первой пустой строке, а в конце файла — NullPointerException | Признак конца файла — только null: while ((line = reader.readLine()) != null) |
new PrintWriter(out, true) и вызов print() | Данные остаются в буфере: autoFlush срабатывает только в println(), printf(), format() | Использовать println() или вызвать flush() |
Не проверять ошибки PrintWriter | Диск заполнен или поток закрыт — исключения нет, данные молча теряются | Вызвать checkError() после записи или использовать BufferedWriter, который бросает IOException |
Что выведет этот код?
PrintWriter pw = new PrintWriter(new StringWriter());
pw.close();
pw.println("после close");
System.out.println(pw.checkError()); Ответ: программа не падает и печатает true. Запись в закрытый PrintWriter внутри вызывает IOException: Stream closed, но println() перехватывает исключение и только выставляет флаг ошибки. Тот же вызов у закрытого BufferedWriter (bw.write("после close")) сразу выбросит IOException. Поэтому PrintWriter удобен для отчётов и консольного вывода, но там, где потеря данных недопустима, ошибки нужно проверять явно.
8. Таблица классов символьных потоков
В пакете java.io около двадцати символьных потоков. Почти у каждого есть байтовый аналог (FileReader — FileInputStream, BufferedWriter — BufferedOutputStream), а классы делятся на источники и приёмники данных (файл, строка, массив, канал) и обёртки, которые добавляют возможности другому потоку.
| Класс | Наследуется от | Назначение |
|---|---|---|
Reader | Object | Абстрактный класс, описывающий символьный поток ввода |
Writer | Object | Абстрактный класс, описывающий символьный поток вывода |
InputStreamReader | Reader | Входной поток, переводящий байты в символы по кодировке |
OutputStreamWriter | Writer | Выходной поток, переводящий символы в байты по кодировке |
FileReader | InputStreamReader | Входной поток, читающий текстовый файл |
FileWriter | OutputStreamWriter | Выходной поток, записывающий текст в файл |
BufferedReader | Reader | Буферизованный входной поток с методами readLine() и lines() |
BufferedWriter | Writer | Буферизованный выходной поток с методом newLine() |
LineNumberReader | BufferedReader | Буферизованный входной поток, который считает прочитанные строки (getLineNumber()) |
PrintWriter | Writer | Выходной поток с методами print(), println(), printf(), format(); не выбрасывает IOException |
StringReader | Reader | Входной поток, читающий из строки String |
StringWriter | Writer | Выходной поток, накапливающий символы в строке; результат — toString() |
CharArrayReader | Reader | Входной поток, читающий из массива char[] |
CharArrayWriter | Writer | Выходной поток, записывающий в растущий массив char[] (toCharArray()) |
FilterReader | Reader | Абстрактный базовый класс для обёрток-фильтров над Reader |
FilterWriter | Writer | Абстрактный базовый класс для обёрток-фильтров над Writer |
PushbackReader | FilterReader | Входной поток, позволяющий вернуть прочитанные символы обратно (unread()) |
PipedReader | Reader | Входной канал для передачи символов между потоками выполнения |
PipedWriter | Writer | Выходной канал, связанный с PipedReader |
Байтовые потоки и их классы — в уроке Типы байтовых потоков в Java. Для двоичных данных (изображения, архивы) символьные потоки не подходят: декодирование по кодировке повредит файл.
Часто задаваемые вопросы
Что выбрать для чтения: BufferedReader или Scanner?
BufferedReader быстрее читает большие файлы построчно: буфер 8192 символа, никакого разбора. Scanner удобнее, когда нужно разбирать числа и слова (nextInt(), nextDouble()), но работает медленнее из-за регулярных выражений и не выбрасывает IOException — ошибку возвращает метод ioException(). Подробнее — в уроке Класс Scanner.
Как прочитать весь текстовый файл в одну строку?
С Java 11 — одной строкой: String text = Files.readString(Path.of("a.txt")); (по умолчанию UTF-8; если в файле некорректные для UTF-8 байты, будет MalformedInputException). Через символьные потоки — reader.lines().collect(Collectors.joining("\n")) у BufferedReader. Для гигабайтных файлов читайте построчно, не загружая всё в память.
Как получить stack trace исключения в виде строки?
Соединить StringWriter и PrintWriter: StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); String trace = sw.toString();. Метод printStackTrace(PrintWriter) пишет трассировку в любой Writer, а StringWriter накапливает её в памяти. Закрывать StringWriter не обязательно — его close() ничего не делает.
Чем PrintWriter отличается от PrintStream (System.out)?
Методы у них почти одинаковые, и оба не выбрасывают IOException. Разница в иерархии: PrintStream — байтовый поток (наследник OutputStream), он сам кодирует символы в байты и умеет писать сырые байты; PrintWriter — символьный (наследник Writer) и может оборачивать любой Writer. System.out и System.err — объекты PrintStream, см. урок Предопределенные потоки ввода-вывода.
Practice
Reinforce this lesson with hands-on tasks, each with a level and a time estimate.
- Копирование файла в другой файл
- Запись отчёта о сотрудниках в файл
- Запись чека в файл через PrintWriter
Comments