1. Introduction
Let’s recall how FileReader and FileWriter work. Every time you call their read() or write() method, there’s a call to the file system — that is, the computer actually hits the disk to read or write a single character. If the file is large and you read or write one character at a time, this turns into a marathon of thousands or even millions of disk accesses. And the disk, as we know, is not a programmer’s fastest friend.
You can think of it like this: you need to pour water from a bucket into a bottle, and you’re doing it with a teaspoon. Technically — it works, but it’s painfully slow. It’s much smarter to use a ladle or a mug. It’s the same with files: reading or writing one character at a time is like hauling water with a spoon.
Here’s what it looks like:
// Reading a file character by character (with a "teaspoon"!)
try (FileReader reader = new FileReader("big.txt")) {
int c;
while ((c = reader.read()) != -1) {
// process the character (e.g., just count it)
}
}
If the file is large, you’ll notice the program runs very slowly.
Buffering: what it is and why you need it
A buffer is a special memory area (usually an array) where data is loaded or written not one character at a time but in large chunks (for example, 8 KB or more). Simply put, a buffer is a piece of memory — something like an intermediate bucket — where data is read from or written to not by single characters, but in larger portions.
Here’s how it works: on read, the program accesses the disk once, loads a whole block of data into the buffer, and then serves it to you character by character from memory. As soon as the block is exhausted, the next one is loaded. On write — the same logic: data is first accumulated in the buffer, and then sent to the disk in one big chunk (or immediately if you call flush()).
Why is this faster? Because disks are slow, especially if you poke them for tiny operations. But if you access them less often and take more at once, everything runs noticeably faster. In short: buffering reduces the number of disk accesses and speeds up your program.
2. BufferedReader and BufferedWriter: syntax and examples
In Java, two classes are used to buffer reading and writing of text files:
- BufferedReader — for reading text files.
- BufferedWriter — for writing text files.
They work on top of regular Reader/Writer (for example, FileReader/FileWriter), adding buffering.
Reading a file line by line with BufferedReader
The most common scenario is to read a file by lines. The readLine() method returns a string up to the newline character ("\n" or "\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); // Print the line to the console
}
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
What’s happening here?
We create a FileReader, which can read a file character by character, and wrap it in a BufferedReader. The buffered reader takes data not one character at a time but in large chunks, stores it in memory, and hands it to us line by line via readLine(). As a result, you just write a while loop, get lines one by one, and don’t worry about how big the file is: reading will still be fast and efficient.
Writing a file with BufferedWriter
Writing lines to a file can be efficient too:
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(); // New line (depends on the OS)
writer.write("This is the second line.");
} catch (IOException e) {
System.out.println("Error writing file: " + e.getMessage());
}
}
}
What’s happening here?
First we create a FileWriter, and then wrap it in a BufferedWriter. When you call write() or newLine(), data does not go straight to disk. It’s accumulated in a buffer — a special memory layer. Only when the buffer fills up, or when you close the stream (or explicitly call flush()), the accumulated text is written to the file in one go. This approach significantly speeds up writing and reduces disk accesses.
What does this look like in a single application?
Suppose we’re writing a simple “diary” program that saves entries to a file and prints them to the screen.
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);
// Writing a new entry
System.out.print("Enter a new diary entry: ");
String entry = scanner.nextLine();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
writer.write(entry);
writer.newLine();
System.out.println("Entry saved!");
} catch (IOException e) {
System.out.println("Error while writing: " + e.getMessage());
}
// Reading all entries
System.out.println("\nYour diary:");
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("Error while reading: " + e.getMessage());
}
}
}
We take the file name "diary.txt" and prompt the user to enter a new entry. To save it, we use a FileWriter in append mode (passing true), so old notes aren’t erased — each new line is neatly added to the end of the file. The BufferedWriter wrapper makes writing fast and efficient: data is first accumulated in memory and then sent to disk in one chunk.
After that, we open the same file for reading via BufferedReader. It grabs the contents in large blocks and hands you lines one by one. In the loop, the program simply prints them to the console, and as a result you see your entire “diary” from start to finish.
3. Advantages of BufferedReader and BufferedWriter
Significant speed-up
When you read or write a file using buffered streams, the program makes tens, and sometimes hundreds, fewer disk accesses. This is especially noticeable on large files.
Convenient methods
- BufferedReader.readLine() — lets you read a file line by line, which is very convenient for processing text files (e.g., logs, CSV, configuration files).
- BufferedWriter.newLine() — adds a newline, correctly inserting the right character depending on the OS.
Ease of use
- The classes are easy to combine with other streams (for example, you can wrap an InputStreamReader inside a BufferedReader to read files with different encodings).
- They automatically close all resources when used with try-with-resources.
Flexibility
- You can explicitly set the buffer size if the default (usually 8 KB) is too small or too large:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // 16 KB buffer
4. When to use BufferedReader and BufferedWriter
Use them when:
- You work with text files (logs, CSV, large text data).
- You need to read or write a file line by line.
- You need to speed up work with large files where performance matters.
- You need to process a data stream from a network or another source that supports Reader/Writer.
Do not use them when:
- Working with binary files (e.g., images, archives, video) — for that use InputStream/OutputStream.
- The file is very small and is read/written once in full — here the gain from buffering will be minimal (but it won’t hurt either).
5. Useful details
Combining with specific encodings
If you need to read/write files in a specific encoding (for example, "UTF-8", "Windows-1251"), use InputStreamReader/OutputStreamWriter together with buffered streams:
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);
Explicitly flushing the buffer
Sometimes it’s important to ensure the data actually hits the disk (for example, if you’re writing a log or a receipt). Call writer.flush() for that. Usually this is not necessary, because closing the stream automatically flushes the buffer.
Buffer size
By default, the buffer is about 8 KB. You can set a different size if you know it will improve performance (for example, when processing huge files).
Comparison: FileReader/FileWriter vs. BufferedReader/BufferedWriter
| Class | Speed on large files | Line-by-line reading convenience | Line-by-line writing convenience | Encoding flexibility |
|---|---|---|---|---|
| FileReader/FileWriter | Slow | No (character-by-character only) | No (character-by-character only) | Default only |
| BufferedReader/Writer | Fast | Yes (readLine()) | Yes (newLine()) | Yes (via InputStreamReader/OutputStreamWriter) |
6. Common mistakes when working with BufferedReader and BufferedWriter
Mistake #1: Forgot to close the stream. If you don’t use try-with-resources or don’t call close(), the file may remain locked and the data may not be written to disk. Always use try-with-resources!
Mistake #2: Mixing up text and binary files. Trying to open a binary file (".jpg", ".zip") via BufferedReader will lead to garbled characters and likely errors. For binary files, use InputStream/OutputStream.
Mistake #3: Not using buffering for large amounts of data. If you read or write character by character, the program will be slow. Always use buffering for large files.
Mistake #4: Not calling flush() when necessary. If you need data to appear on disk immediately (for example, for logging), call writer.flush(). But in most cases closing the stream will do it for you.
Mistake #5: Ignoring encoding. If you open a file with the wrong encoding, text may be displayed incorrectly (letters turn into “?”, ideographs, etc.). Always explicitly specify the required encoding if it differs from the system one (for example, "UTF-8").
GO TO FULL VERSION