I/O ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-31

Классы символьных потоков в 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() понимает все три варианта.

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.

All module tasks →

Comments

Please log in or register to have a possibility to add comment.