Topic 12.2
Buffered Readers and Writers
In one line
BufferedReader and BufferedWriter put an in-memory array between your code and the real source, so thousands of tiny reads or writes become a few big ones. BufferedReader also gives you readLine() and lines(), the standard way to read text line by line.
Think of it like this
Fetching water from a well. You could walk to the well for every sip, or fill a big bucket once and drink from it all day. The walk is the expensive part. A buffer is the bucket: BufferedReader fetches a big chunk of characters (8192 by default) in one trip to the source, then hands them to you one by one from memory. BufferedWriter does the reverse: it collects what you write and carries it to the destination in one trip.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Buffer
- A block of memory that holds data temporarily, so it can be moved in a few big pieces instead of many small ones.
- System call
- A request from a program to the operating system, like "read 8 KB from this file". It is much slower than reading memory.
- Line terminator
- The invisible characters that end a line of text:
\non Linux and macOS,\r\non Windows. - Flush
- Push everything waiting in a buffer out to the real destination now, instead of waiting for the buffer to fill.
- Lazy
- Done only when needed. A lazy stream of lines reads the next line only when you ask for it.
- Token
- One piece of input between separators, such as one word or one number in
"12 apples". - Locale
- A setting for a language and country that controls things like whether a decimal number is written
3.5or3,5. - StringReader / StringWriter
- A
Readerthat reads from aString, and aWriterthat collects text into a string, both entirely in memory.
Step by step
01Why buffering exists
Every read() on a FileReader can go all the way down to the operating system. BufferedReader instead calls the underlying reader's read(char[], off, len) with an 8192-character array, then serves your next 8191 read() calls from that array without touching the source.
The example "Count the trips to the source" below measures it: reading 20,000 characters unbuffered makes 20,001 calls to the source; buffered, it makes 4.
02Reading lines
The standard loop reads a line, checks it for null, and uses it, all in one expression. Call readLine() once per iteration; calling it twice skips every other line (see Break it).
readLine() strips the terminator, so you never see \r or \n in the result, whichever operating system wrote the file.
try (BufferedReader reader = new BufferedReader(new StringReader(text))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[" + line + "]");
}
}03Lines as a stream (Java 8)
reader.lines() gives a Stream<String>. It is lazy: lines are read only as the stream pulls them, so limit(10) reads only about ten lines from a huge file (plus whatever the buffer pre-fetched).
The stream doesn't own the reader: close the reader with try-with-resources. Files.lines(path) (Topic 12.3) is different: that stream owns the file and must itself be closed.
try (BufferedReader reader = Files.newBufferedReader(Path.of("orders.csv"))) {
long big = reader.lines()
.skip(1) // header
.map(l -> l.split(","))
.filter(f -> Integer.parseInt(f[2]) > 500)
.count();
}04Writing text with a buffer
Stack a PrintWriter on a BufferedWriter for convenient, fast output: printf and println from the first, the 8192-character buffer from the second.
BufferedWriter.newLine() and PrintWriter.println() write the platform line separator. If the file must have \n everywhere (for example a file read by Linux tools, or a test comparing exact bytes), write "\n" yourself.
try (PrintWriter out = new PrintWriter(
Files.newBufferedWriter(Path.of("report.txt")))) { // UTF-8, buffered
out.printf(Locale.ROOT, "%-6s %8.2f%n", "tea", 25.0);
out.println("total: 1");
} // close() flushes the buffer, then closes the file05What close() and flush() really do
flush() asks each layer to pass its buffered data down: PrintWriter flushes the BufferedWriter, which writes its array to the OutputStreamWriter, which encodes it and writes bytes to the FileOutputStream, which makes the system call. Even then the data may sit in the operating system's page cache; only FileChannel.force(true) or FileDescriptor.sync() forces it onto the disk.
close() flushes and then releases the file handle. Once closed, a BufferedReader throws IOException: Stream closed on the next read.
06Test with StringReader and StringWriter
If a method accepts a Reader or BufferedReader instead of a file name, it can be tested with new StringReader("a\nb") and no files at all. Likewise, a method that writes to a Writer can be tested with a StringWriter and toString().
This is the dependency injection idea applied to I/O: let the caller decide where the data comes from.
static int countWords(BufferedReader in) throws IOException {
int words = 0;
String line;
while ((line = in.readLine()) != null) {
if (!line.isBlank()) words += line.trim().split("\\s+").length;
}
return words;
}
// production: countWords(Files.newBufferedReader(path))
// test: countWords(new BufferedReader(new StringReader("a b\n c")))07Scanner versus BufferedReader
Scanner reads tokens and parses them. It's handy for "3 apples 2.5", but each next...() call runs a regular expression, so it is several times slower than readLine() plus Integer.parseInt on large input. That's why competitive programmers and the DSA course (/dsa) use BufferedReader for fast input.
Scanner.nextDouble() uses the default locale. Call scanner.useLocale(Locale.ROOT) when parsing machine-written numbers, or you'll get InputMismatchException on machines set to a locale that writes 3,5.
Try it yourself
- 1
Change the buffer size
In "Count the trips to the source", use
new BufferedReader(counted, 100). Predict the number of calls (20,000 / 100 plus one), then run it. - 2
Find where the flush happens
In "Nothing arrives until you flush", replace
new BufferedWriter(target)withnew BufferedWriter(target, 16). Predict the "before flush" count now: the buffer can only hold 16 characters, so it must spill. - 3
Count lines lazily
In the stream example, add
.peek(l -> System.out.println("read: " + l))right afterlines()and replace the collector with.findFirst(). Predict how many "read:" lines appear.
Code & diagrams
\n, \r\n and a lone \r all end a line, and none of them appear in the result. An empty line is "", not null.
Expected output
1: [first] length 5
2: [second] length 6
3: [] length 0
4: [fourth] length 6
5: [last without newline] length 20
after the end: nullBuffered: 8192 + 8192 + 3616 characters, then one call that returns -1. With a FileReader, each source call can be a system call.
Expected output
unbuffered: 20000 chars, 20001 calls to the source
buffered: 20000 chars, 4 calls to the sourceThe text is held in BufferedWriter's array until flush() or close(). PrintWriter hides I/O errors; checkError() is the only way to see them.
Expected output
before flush: 0 chars
after flush: 32 chars
tea | 25.00
samosa| 12.50
checkError() after writing to a closed writer: trueExpected output
{Delhi=380, Pune=570}useLocale(Locale.ROOT) makes nextDouble() expect a dot as the decimal separator, whatever the machine's settings.
Expected output
3 x apples @ 2.5
7 x mangoes @ 1.25
total: 16.25Terminal: java Main.java prints "> buy milk" and "> call Ravi". Files.newBufferedReader/Writer default to UTF-8, unlike FileReader/FileWriter before Java 18.
import java.io.*;
import java.nio.file.*;
public class Main {
public static void main(String[] args) throws IOException {
Path file = Path.of("notes.txt");
try (BufferedWriter w = Files.newBufferedWriter(file)) { // UTF-8, creates or truncates
w.write("buy milk");
w.newLine();
w.write("call Ravi");
w.newLine();
}
try (BufferedReader r = Files.newBufferedReader(file)) { // UTF-8
String line;
while ((line = r.readLine()) != null) {
System.out.println("> " + line);
}
}
}
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call readLine() twice per loop
Write the loop as while (reader.readLine() != null) { System.out.println(reader.readLine()); } over the text "a\nb\nc\nd\ne".
Break #2
Never close the writer
Write a report with BufferedWriter w = Files.newBufferedWriter(path); w.write("total: 42"); and let main end without close().
Break #3
Read a UTF-8 file with the default charset
On Windows with Java 17, read a UTF-8 file containing café with new BufferedReader(new FileReader("menu.txt")).
Myth vs fact
Myth
BufferedReader reads the whole file into memory.
Fact
It holds only its buffer, 8192 characters by default. It can read a 50 GB file line by line with a few kilobytes of memory, as long as you don't collect all the lines yourself.
Myth
println writes to the file immediately.
Fact
With a buffered writer, the text waits in memory until the buffer fills or you flush or close. Even after a flush the OS may cache it before writing to disk.
Myth
readLine() returns null for an empty line.
Fact
An empty line comes back as "". Only the end of the input gives null.
Myth
Bigger buffers are always faster.
Fact
Past roughly 8 to 64 KB the gain is small, because the operating system already reads ahead. Huge buffers just waste memory, especially with thousands of open readers.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
BufferedReaderisn'tBufferedInputStreamplus decoding: it buffers chars, while theInputStreamReaderunderneath has its own byte buffer (8192 bytes inStreamDecoder). Sonew BufferedReader(new InputStreamReader(in))already has two buffers; adding aBufferedInputStreamunderneath is usually wasted. - ▸
readLine()builds a newStringper line, which is fine for most files but creates a lot of garbage on multi-gigabyte inputs. High-throughput parsers read into achar[]or useFileChannelwith aByteBufferand parse bytes directly, avoiding decoding entirely for ASCII data. - ▸
Since JDK 19,
BufferedReader,BufferedWriterand similar classes lock with an internalReentrantLockinstead ofsynchronizedwhen you use the JDK class itself (not a subclass), so a virtual thread blocked inreadLine()doesn't pin its carrier thread (Topic 13.10). Readers are still not meant to be shared between threads without coordination. - ▸
Java 10 added
Reader.transferTo(Writer)and Java 11 addedReader.nullReader()andWriter.nullWriter().StringWriteris backed by aStringBuffer(synchronized), so for single-threaded string buildingStringBuilderis a little faster.
Remember this
- 1
The "walk to the well" is a system call: a request from your program to the operating system kernel, such as
readorwriteon a file. A system call costs hundreds of nanoseconds to microseconds, while reading acharfrom an array costs about a nanosecond. An unbufferedFileReadercan make a system call for every few characters; reading a 100 MB file that way can take minutes instead of a second. Buffering changes the number of calls, not the result. - 2
BufferedReaderwraps anyReader. Besidesread(), it addsString readLine(), which returns one line without its line terminator, andnullat the end of the input. It understands all three terminators:\n(Linux, macOS),\r\n(Windows) and a lone\r. The last line is returned even if it has no terminator. An empty line comes back as"", which is different fromnull. - 3
BufferedReader.lines()(Java 8) returns aStream<String>of the lines, so you canfilter,mapandcollectthem (Topic 10.4). The stream reads lazily from the reader as you consume it. Wrapping I/O errors that happen during the stream, it throwsUncheckedIOException, because stream lambdas can't throw checked exceptions. - 4
BufferedWriterwraps anyWriterand addsnewLine(), which writesSystem.lineSeparator()(\r\non Windows,\nelsewhere). Its data sits in memory until the buffer fills, or you callflush()orclose(). Forgetting to close a writer is the number one cause of empty or cut-off output files.close()flushes first, so try-with-resources (Topic 7.6) fixes it. - 5
PrintWriteraddsprint,printlnandprintfon top of anyWriter. It never throwsIOException; it records failure and you checkcheckError().StringReaderandStringWriterare character streams over aStringin memory, perfect for tests: code that accepts aReadercan be tested without creating a file. - 6
Scanner(Java 5) is another way to read text: it splits input into tokens and parses numbers (nextInt(),nextDouble()). It is convenient for small inputs, but slower thanBufferedReader(it uses regular expressions) and its number parsing depends on the locale, so3.5fails under a German locale that expects3,5. For big inputs, useBufferedReaderand parse yourself. In real code you get a buffered reader for a file withFiles.newBufferedReader(path)(Topic 12.3), which defaults to UTF-8.
Explain it without notes
Why is buffered I/O faster, and what exactly does the buffer save?
How does readLine() treat line terminators, empty lines and the last line?
What's the difference between flush() and close(), and why do output files end up empty?
When would you choose Scanner over BufferedReader, and what are its pitfalls?
Practice
Read "apple\n\nbanana\ncherry\n" with a BufferedReader and print how many lines there are, how many are blank, and the longest line.
Write a method void writeTable(Writer out, String[] names, int[] scores) that prints aligned rows with PrintWriter (names left-aligned in 8 characters, scores right-aligned in 4). Test it with a StringWriter and print the result.
Using BufferedReader.lines(), count the words in a multi-line string, ignoring blank lines and extra spaces.
Trade-offs
- ↔
Buffering trades memory and latency for throughput: data waits in memory before it's written. For logs or network protocols where the other side must see data promptly, flush at message boundaries (or use autoflush), accepting more system calls.
- ↔
readLine()is simple and fast enough for most text, but it allocates aStringper line and can't handle a single line larger than memory. Very large or binary-ish inputs need chunked reads onchar[]/byte[]. - ↔
Scannermakes parsing easy at the cost of speed and locale surprises;BufferedReaderplus manual parsing is faster and predictable but more code.
Done when you can
Done when you can explain what a buffer saves and roughly how many source calls a buffered read makes.
Done when you can write the
readLine()loop correctly and know whatnulland""mean.Done when every writer you open is closed by try-with-resources, and you know when an explicit
flush()is needed.Done when you can process lines with
lines()and streams.Done when you can test I/O code with
StringReaderandStringWriterinstead of real files.