BufferedReader & Writer
BufferedReader & Writer
BufferedReader and BufferedWriter wrap another Reader or Writer and read or write in larger internal chunks, cutting down the number of actual I/O calls made against the underlying file, while also adding the convenience methods — readLine() and newLine() — that make line-oriented text processing far less tedious to write by hand.
What Is Buffering?
Buffering means reading or writing a large internal chunk of data at once instead of one character at a time, then serving individual characters or lines out of that in-memory chunk. Reading a file line by line with a raw FileReader means manually watching for the '\n' character and handling the file's final line, which has no trailing terminator, as a special case.
1// File: BeforeBufferedReader.java
2import java.io.*;
3import java.nio.file.*;
4
5public class BeforeBufferedReader {
6 public static void main(String[] args) throws IOException {
7 Path tempFile = Files.createTempFile("before-buffered", ".txt");
8 Files.writeString(tempFile, "apple\nbanana\ncherry");
9
10 StringBuilder currentLine = new StringBuilder();
11 int lineCount = 0;
12 try (FileReader reader = new FileReader(tempFile.toFile())) {
13 int ch;
14 while ((ch = reader.read()) != -1) {
15 if (ch == '\n') {
16 lineCount++;
17 currentLine.setLength(0);
18 } else {
19 currentLine.append((char) ch);
20 }
21 }
22 if (currentLine.length() > 0) {
23 lineCount++;
24 }
25 }
26
27 System.out.println("Lines: " + lineCount);
28
29 Files.delete(tempFile);
30 }
31}Output:
Lines: 3
BufferedReader.readLine() handles line-terminator detection, including that same final-line edge case, internally — and reads in larger chunks under the hood instead of one character at a time.
1// File: AfterBufferedReader.java
2import java.io.*;
3import java.nio.file.*;
4
5public class AfterBufferedReader {
6 public static void main(String[] args) throws IOException {
7 Path tempFile = Files.createTempFile("after-buffered", ".txt");
8 Files.writeString(tempFile, "apple\nbanana\ncherry");
9
10 int lineCount = 0;
11 try (BufferedReader reader = new BufferedReader(new FileReader(tempFile.toFile()))) {
12 while (reader.readLine() != null) {
13 lineCount++;
14 }
15 }
16
17 System.out.println("Lines: " + lineCount);
18
19 Files.delete(tempFile);
20 }
21}Output:
Lines: 3
Both count the same three lines, but readLine() removes the manual character-by-character bookkeeping entirely.
How It Works Internally
One sentence before the diagram: the difference between buffered and unbuffered I/O comes down entirely to how many times the JVM actually has to ask the operating system for more data.
Unbuffered FileReader reading "apple\nbanana": read() -> 'a' [system call] read() -> 'p' [system call] read() -> 'p' [system call] ... one system call PER CHARACTER, 12 total for 12 characters BufferedReader reading the same text: fill internal buffer (8192 chars) [ONE system call] readLine() -> "apple" [served from the in-memory buffer] readLine() -> "banana" [served from the in-memory buffer]
A raw FileReader making one system call per character is exactly why wrapping it matters the moment more than a trivial amount of data is involved — each system call carries real overhead, and a buffered reader amortizes that cost across an entire chunk instead of paying it per character.
BufferedWriter mirrors BufferedReader on the write side — write() for raw text and newLine() for a platform-appropriate line terminator, rather than hardcoding "\n" directly.
1// File: BufferedWriterExample.java
2import java.io.*;
3import java.nio.file.*;
4
5public class BufferedWriterExample {
6 public static void main(String[] args) throws IOException {
7 Path tempFile = Files.createTempFile("buffered-writer-demo", ".txt");
8
9 try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toFile()))) {
10 writer.write("first");
11 writer.newLine();
12 writer.write("second");
13 }
14
15 try (BufferedReader reader = new BufferedReader(new FileReader(tempFile.toFile()))) {
16 String line;
17 while ((line = reader.readLine()) != null) {
18 System.out.println("Line: " + line);
19 }
20 }
21
22 Files.delete(tempFile);
23 }
24}Output:
Line: first
Line: second
A BufferedWriter does not send data to disk the moment write() is called — it accumulates in memory until the buffer fills, flush() runs, or close() runs. This is covered concretely in this article's Common Mistakes section.
readLine() recognizes whatever line terminator newLine() actually wrote, so this works identically regardless of the platform it runs on.
Real-World Example
A log analysis tool reads a server's raw log file line by line and writes only the error lines to a separate file, using BufferedReader and BufferedWriter together in a single try-with-resources statement — the exact combination that puts both buffering behaviors to work at once.
1// File: LogErrorExtractor.java
2import java.io.*;
3import java.nio.file.*;
4
5public class LogErrorExtractor {
6
7 public int extractErrors(Path sourceLog, Path errorLog) throws IOException {
8 int errorCount = 0;
9
10 try (BufferedReader reader = new BufferedReader(new FileReader(sourceLog.toFile()));
11 BufferedWriter writer = new BufferedWriter(new FileWriter(errorLog.toFile()))) {
12
13 String line;
14 while ((line = reader.readLine()) != null) {
15 if (line.contains("ERROR")) {
16 writer.write(line);
17 writer.newLine();
18 errorCount++;
19 }
20 }
21 }
22
23 return errorCount;
24 }
25}1// File: LogErrorExtractorDemo.java
2import java.io.IOException;
3import java.nio.file.*;
4
5public class LogErrorExtractorDemo {
6 public static void main(String[] args) throws IOException {
7 Path sourceLog = Files.createTempFile("server", ".log");
8 Files.writeString(sourceLog, """
9 INFO Server started
10 ERROR Connection refused on port 8080
11 INFO Request handled in 45ms
12 ERROR Timeout waiting for database
13 INFO Server shutting down
14 """);
15
16 Path errorLog = sourceLog.resolveSibling("errors-only.log");
17
18 LogErrorExtractor extractor = new LogErrorExtractor();
19 int count = extractor.extractErrors(sourceLog, errorLog);
20
21 System.out.println("Errors found: " + count);
22 System.out.print(Files.readString(errorLog));
23
24 Files.delete(sourceLog);
25 Files.delete(errorLog);
26 }
27}Output:
Errors found: 2
ERROR Connection refused on port 8080
ERROR Timeout waiting for database
A mistake that appears often in fresher pull requests is opening a BufferedReader and a BufferedWriter for the same batch job in two separate try-with-resources blocks stacked one after another, when a single block declaring both resources — separated by a semicolon, exactly as extractErrors does here — closes both in the correct order automatically, even if one of them throws partway through.
Best Practices
Wrap any FileReader or FileWriter used for more than a trivial amount of data in BufferedReader or BufferedWriter — the cost of wrapping is negligible and the benefit compounds with file size.
Check readLine() != null as the loop-termination condition, never against an empty string, since a genuinely blank line in the file is a valid, non-null return value distinct from reaching the end of the file.
Use newLine() instead of hardcoding "\n" when writing line-oriented output, so the generated file's line terminators match the platform's own convention.
Declare related reader and writer resources together in one try-with-resources statement, as this article's real-world example does, rather than nesting or stacking separate blocks.
Common Mistakes
Confusing readLine()'s null return at end-of-file with an empty string is a classic source of an infinite loop — a condition like while (!reader.readLine().equals("")) never terminates normally at end-of-file, and throws a NullPointerException instead the moment readLine() actually returns null.
1// Illustrative only - do not run: this loop never terminates correctly
2// at end-of-file, since readLine() returns null there, not ""
3while (!reader.readLine().equals("")) {
4 // process the line
5}Assuming data is written to disk as soon as write() is called overlooks that a BufferedWriter holds content in memory until its buffer fills, is explicitly flushed, or is closed.
1// File: ForgottenCloseMistake.java
2import java.io.*;
3import java.nio.file.*;
4
5public class ForgottenCloseMistake {
6 public static void main(String[] args) throws IOException {
7 Path tempFile = Files.createTempFile("forgotten-close", ".txt");
8
9 BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toFile()));
10 writer.write("buffered content");
11 System.out.println("Before close: [" + Files.readString(tempFile) + "]");
12
13 writer.close();
14 System.out.println("After close: [" + Files.readString(tempFile) + "]");
15
16 Files.delete(tempFile);
17 }
18}Output:
Before close: []
After close: [buffered content]
The file on disk is genuinely empty right up until close() runs, since "buffered content" is far smaller than the writer's internal buffer and nothing has forced a flush yet — if the program crashed between the write() call and close(), that content would never reach the file at all.
Interview Questions
Q1. Why does BufferedReader/BufferedWriter exist when FileReader/FileWriter can already read and write?
They reduce the number of actual I/O operations against the underlying file by reading or writing in larger internal chunks, and they add convenience methods — readLine() and newLine() — that a raw FileReader or FileWriter does not provide at all. The nuance interviewers are listening for is whether you can explain the system-call cost buffering avoids, not just recite that it's "faster."
Q2. What does readLine() return at the end of a file, and why does this matter for loop termination?
It returns null, not an empty string. A loop must check for null specifically to terminate correctly — checking against "" either loops forever on a file with no blank lines or throws a NullPointerException the moment readLine() actually reaches the end of the file. This is one of the most common fresher mistakes, which is exactly why it gets asked so often.
Q3. What is the difference between BufferedWriter.newLine() and hardcoding "\n"?
newLine() writes whatever line separator is appropriate for the platform the code is running on, while a hardcoded "\n" always writes exactly that character regardless of platform convention. readLine() on the reading side recognizes either terminator correctly, which is why this distinction rarely causes a functional bug, only a stylistic inconsistency.
Q4. What happens to data written through a BufferedWriter if the program exits before close() or flush() is called?
It can be lost entirely. Data written through a BufferedWriter sits in an in-memory buffer until the buffer fills, flush() is called explicitly, or close() runs — an abnormal exit before any of those happens means the buffered content never reaches the underlying file. Product-company interviewers often follow up asking how this affects crash-recovery design, since it's a real production concern, not just trivia.
Q5. Can BufferedReader wrap something other than a FileReader?
Yes. BufferedReader's constructor accepts any Reader, including an InputStreamReader wrapping System.in for buffered console input, or a StringReader wrapping an in-memory String. The nuance being tested is whether you understand buffering as a general decorator pattern, not something specific to files.
Q6. What is the default buffer size used by BufferedReader and BufferedWriter?
8192 characters by default for both, though each also has a constructor overload accepting an explicit buffer size for cases where a different size is genuinely beneficial. Interviewers rarely need the exact number memorized, but listen for whether you know a default exists and can be overridden.
Q7. Can multiple resources be declared in a single try-with-resources statement?
Yes, separated by semicolons, exactly as LogErrorExtractor does with its BufferedReader and BufferedWriter in this article's real-world example — resources declared this way are closed automatically in the reverse of their declaration order. The nuance interviewers are listening for is that close order, since it matters when resources depend on each other.
FAQs
Does wrapping a reader in BufferedReader change what methods are available?
Yes, it adds readLine() on top of whatever the wrapped Reader already provided, which is the main reason to use it beyond the performance benefit of buffering itself.
Is BufferedReader necessary when using Files.lines() or Files.readAllLines()?
No. Both of those Files methods, covered in this section's Reading Files article, already use buffered I/O internally, so wrapping their result in an additional BufferedReader would be redundant.
Does closing a BufferedWriter automatically flush any buffered data first?
Yes. close() flushes any remaining buffered content before actually closing the underlying writer, which is why the ForgottenCloseMistake example above shows the file's full content appearing immediately once close() is called.
Can BufferedReader be used to read from System.in?
Yes, new BufferedReader(new InputStreamReader(System.in)) is a standard pattern for reading a user's console input one line at a time with readLine().
Does readLine() include the line terminator in the returned string?
No. Whatever terminator was found — \n, \r, or \r\n — is stripped from the returned line, matching the same convention Files.readAllLines() uses internally.
Is it necessary to wrap a Scanner in a BufferedReader for performance?
It can help. Scanner does not itself buffer reads from its underlying source as efficiently as BufferedReader does, so passing a BufferedReader-wrapped source into a Scanner instead of a raw FileReader can reduce the number of underlying I/O calls it triggers, particularly for a large file.
What happens if flush() is called manually partway through writing?
Any content currently sitting in the internal buffer is written out to the underlying file immediately, without closing the writer — writing can continue normally afterward, which is useful when a partial result needs to become visible on disk before the writer is done.
Summary
BufferedReader and BufferedWriter wrap a plain character stream to reduce the number of actual I/O operations and add the line-oriented convenience methods, readLine() and newLine(), that make text processing far less tedious than working with a raw FileReader or FileWriter directly.
The habit worth carrying forward from this article's log extraction example is declaring related reader and writer resources together in one try-with-resources statement, and never assuming a BufferedWriter's content has reached disk until flush() or close() has actually run.
What to Read Next
Learn Java's modern, more flexible file API.