1. Введение
Давайте вспомним, как работают FileReader и FileWriter. Каждый раз, когда вы вызываете у них метод read() или write(), происходит обращение к файловой системе — то есть компьютер реально лезет на диск, чтобы прочитать или записать один символ. Если файл большой, а вы читаете или пишете по одному символу за раз, это превращается в марафонскую дистанцию из тысяч или даже миллионов обращений к диску. А диск, как известно, — не самый быстрый друг программиста.
Можно представить это так: вам нужно перелить воду из ведра в бутылку, и вы делаете это чайной ложкой. Формально — работает, но ужасно долго. Куда разумнее взять ковшик или кружку. То же самое и с файлами: читать или писать по одному символу — всё равно что таскать воду ложкой.
Вот как это происходит:
// Чтение файла посимвольно (по "чайной ложке"!)
try (FileReader reader = new FileReader("big.txt")) {
int c;
while ((c = reader.read()) != -1) {
// обрабатываем символ (например, просто считаем)
}
}
Если файл большой, вы заметите, что программа работает ну очень неспешно.
Буферизация: что это и зачем нужна
Буфер — это специальная область памяти (обычно массив), куда данные загружаются или записываются не по одному символу, а сразу большими кусками (например, по 8 килобайт или больше). Говоря проще, буфер — это кусок памяти, что-то вроде промежуточного ведра, куда данные читаются или записываются не по одному символу, а сразу большими порциями.
Работает это так: при чтении программа один раз обращается к диску, загружает в буфер целый блок данных и дальше спокойно раздаёт их вам по одному символу из памяти. Как только блок заканчивается, подгружается следующий. При записи — та же логика: данные сначала складываются в буфер, а уже потом одним большим куском отправляются на диск (или сразу, если вы вызвали flush()).
Почему это быстрее? Потому что диск сам по себе медленный, особенно если дёргать его по мелочи. А вот если сходить к нему реже, но за раз взять побольше, всё работает заметно шустрее. Проще говоря: буферизация экономит количество обращений к диску и ускоряет работу программы.
2. BufferedReader и BufferedWriter: синтаксис и примеры
В Java для буферизации чтения и записи текстовых файлов используются два класса:
- BufferedReader — для чтения текстовых файлов.
- BufferedWriter — для записи текстовых файлов.
Они работают поверх обычных Reader/Writer (например, FileReader/FileWriter), добавляя буферизацию.
Чтение файла построчно с помощью BufferedReader
Самый частый сценарий — читать файл по строкам. Метод readLine() возвращает строку до символа новой строки ("\n" или "\r\n").
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // Выводим строку на экран
}
} catch (IOException e) {
System.out.println("Ошибка при чтении файла: " + e.getMessage());
}
}
}
Что здесь происходит?
Мы создаём FileReader, который умеет читать файл символ за символом, и оборачиваем его в BufferedReader. Буферизованный читатель берёт данные не по одному символу, а сразу большими кусками, складывает их в память и раздаёт нам построчно с помощью метода readLine(). В итоге вы просто пишете цикл while, получаете строки одну за другой и не думаете о том, насколько большой файл: чтение всё равно будет быстрым и экономным.
Запись файла с помощью BufferedWriter
Записывать строки в файл тоже можно эффективно:
import java.io.*;
public class BufferedWriterExample {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write("Hello, world!");
writer.newLine(); // Перевод строки (зависит от ОС)
writer.write("Это вторая строка.");
} catch (IOException e) {
System.out.println("Ошибка при записи файла: " + e.getMessage());
}
}
}
Что здесь происходит?
Сначала мы создаём FileWriter, а затем оборачиваем его в BufferedWriter. Когда вы вызываете write() или newLine(), данные не бегут сразу на диск. Они складываются в буфер — специальную память-прослойку. Лишь когда буфер заполнится, либо вы закроете поток (или явно вызовете flush()), весь накопленный текст разом записывается в файл. Такой подход значительно ускоряет запись и экономит обращения к диску.
Как это выглядит в едином приложении?
Допустим, мы пишем простую программу-«дневник», которая сохраняет записи в файл и выводит их на экран.
import java.io.*;
import java.util.Scanner;
public class DiaryApp {
public static void main(String[] args) {
String fileName = "diary.txt";
Scanner scanner = new Scanner(System.in);
// Запись новой записи
System.out.print("Введите новую запись в дневник: ");
String entry = scanner.nextLine();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
writer.write(entry);
writer.newLine();
System.out.println("Запись сохранена!");
} catch (IOException e) {
System.out.println("Ошибка при записи: " + e.getMessage());
}
// Чтение всех записей
System.out.println("\nВаш дневник:");
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Ошибка при чтении: " + e.getMessage());
}
}
}
Мы берём имя файла "diary.txt" и предлагаем пользователю ввести новую запись. Для сохранения используем FileWriter в режиме append (передаём true), так что старые заметки не стираются — каждая новая строка аккуратно добавляется в конец файла. Обёртка в виде BufferedWriter делает запись быстрой и экономной: данные сначала накапливаются в памяти, а потом одним куском уходят на диск.
После этого мы открываем тот же файл на чтение через BufferedReader. Он берёт содержимое большими блоками, а вам выдаёт строки по одной. В цикле программа просто выводит их на экран, и в итоге вы видите весь свой «дневник» от начала до конца.
3. Преимущества BufferedReader и BufferedWriter
Существенное ускорение работы
Когда вы читаете или пишете файл с помощью буферизированных потоков, программа делает в десятки, а иногда и в сотни раз меньше обращений к диску. Это особенно заметно на больших файлах.
Удобные методы
- BufferedReader.readLine() — позволяет читать файл по строкам, что очень удобно для обработки текстовых файлов (например, логов, CSV, конфигов).
- BufferedWriter.newLine() — добавляет перевод строки, корректно подставляя нужный символ в зависимости от ОС.
Простота использования
- Классы легко комбинируются с другими потоками (например, можно обернуть InputStreamReader внутри BufferedReader для чтения файлов с разной кодировкой).
- Автоматически закрывают все ресурсы при использовании try-with-resources.
Гибкость
- Можно задать размер буфера явно, если по умолчанию (обычно 8 КБ) вам мало или много:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // буфер на 16 КБ
4. Когда использовать BufferedReader и BufferedWriter
Используйте их:
- Если работаете с текстовыми файлами (логи, CSV, большие текстовые данные).
- Когда нужно читать или писать файл построчно.
- Для ускорения работы с большими файлами, когда скорость важна.
- Если нужно обрабатывать поток данных из сети или другого источника, поддерживающего Reader/Writer.
Не используйте их:
- Для работы с бинарными файлами (например, картинки, архивы, видео) — для этого есть InputStream/OutputStream.
- Если файл очень маленький и читается/записывается один раз целиком — здесь выигрыш от буферизации будет минимальным (но и вреда не будет).
5. Полезные нюансы
Комбинирование с разными кодировками
Если вам нужно читать/писать файлы в определённой кодировке (например, "UTF-8", "Windows-1251"), используйте InputStreamReader/OutputStreamWriter в связке с буферизированными потоками:
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);
Явный сброс буфера
Иногда важно, чтобы данные точно попали на диск (например, если пишете лог или чек). Для этого вызывайте writer.flush(). Обычно это не нужно, так как закрытие потока автоматически сбрасывает буфер.
Размер буфера
По умолчанию буфер — около 8 КБ. Можно задать другой размер, если вы точно знаете, что это даст прирост производительности (например, при обработке гигантских файлов).
Сравнение: FileReader/FileWriter vs BufferedReader/BufferedWriter
| Класс | Скорость на больших файлах | Удобство чтения по строкам | Удобство записи по строкам | Гибкость с кодировками |
|---|---|---|---|---|
| FileReader/FileWriter | Медленно | Нет (только по символу) | Нет (только по символу) | Только по умолчанию |
| BufferedReader/Writer | Быстро | Да (readLine()) | Да (newLine()) | Да (через InputStreamReader/OutputStreamWriter) |
6. Типичные ошибки при работе с BufferedReader и BufferedWriter
Ошибка №1: Забыли закрыть поток. Если не использовать try-with-resources или не вызвать close(), файл может остаться заблокированным, а данные — не записанными на диск. Используйте всегда try-with-resources!
Ошибка №2: Путают работу с текстовыми и бинарными файлами. Попытка открыть бинарный файл (".jpg", ".zip") через BufferedReader приведёт к «кракозябрам» и, скорее всего, к ошибкам. Для бинарных файлов используйте InputStream/OutputStream.
Ошибка №3: Не используют буферизацию при больших объёмах данных. Если читать или писать по символу, программа будет работать медленно. Всегда используйте буферизацию для больших файлов.
Ошибка №4: Не вызывают flush() при необходимости. Если нужно, чтобы данные появились на диске немедленно (например, для логирования), вызывайте writer.flush(). Но обычно закрытие потока всё сделает за вас.
Ошибка №5: Не учитывают кодировку. Если открыть файл с неправильной кодировкой, текст может отображаться некорректно (русские буквы превращаются в «?», иероглифы и т.д.). Всегда явно указывайте нужную кодировку, если она отличается от системной (например, "UTF-8").
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ