Java Buffered Streams
Speed up Java I/O with buffered streams — BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream.
Java Buffered Streams
The byte- and character-stream chapters described the raw APIs honestly: every call to FileInputStream.read() or FileReader.read() is a syscall. A syscall takes on the order of a microsecond — fast in isolation, catastrophic in a tight loop. Reading a 1 MB file one byte at a time is a million syscalls; the same file with an 8 KB buffer is 128. The wall-clock difference is two or three orders of magnitude.
The Buffered* decorators sit between your code and the raw stream. They keep an in-memory byte[] (or char[]) and serve read() calls out of it, only going to the OS when the buffer is empty. On the write side they accumulate small writes in a buffer and only write() to the OS when the buffer fills or you call flush/close. Same API, completely different cost.
The four buffered classes
| Class | Wraps |
|---|---|
BufferedInputStream | An InputStream. Adds an internal byte[] buffer. |
BufferedOutputStream | An OutputStream. Adds an internal byte[] buffer. |
BufferedReader | A Reader. Adds an internal char[] buffer and the famous readLine() method. |
BufferedWriter | A Writer. Adds an internal char[] buffer and a newLine() method. |
All four wrap any stream of the matching kind — file, socket, pipe, in-memory — not just file streams:
BufferedInputStream in = new BufferedInputStream(new FileInputStream(path.toFile()));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(path.toFile()));
BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));Default buffer size is 8192 bytes/chars — chosen to match common OS page sizes. You can pass a different size to the second constructor, but the default is fine in virtually every case. Bigger buffers don't speed things up linearly; they just use more memory.
The modern API hands you these decorators preassembled:
BufferedReader r = Files.newBufferedReader(path); // UTF-8 by default
BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8);
InputStream in = new BufferedInputStream(Files.newInputStream(path));
OutputStream out = new BufferedOutputStream(Files.newOutputStream(path));Files.newBufferedReader / Files.newBufferedWriter already wrap the bridge class with the right charset and a BufferedReader/BufferedWriter. For text, that's the one-line replacement for the three-deep manual stack.
BufferedReader.readLine()
The reason BufferedReader is the most-used class in java.io:
String readLine() throws IOException; // a line, terminator stripped, or null at end
Stream<String> lines(); // Java 8+: line streamreadLine recognises \n, \r, and \r\n as line terminators and returns the line without the terminator. It returns null (not an empty string, not -1) at end-of-stream — the standard read-line idiom:
try (BufferedReader r = Files.newBufferedReader(path)) {
String line;
while ((line = r.readLine()) != null) {
process(line);
}
}r.lines() returns a Stream<String> for the functional pipeline form. The stream owns the open Reader, so the try-with-resources around the reader still does the closing work — lines() itself doesn't need its own close.
Two things to know about readLine(). First, it allocates a String per line. For tight log-processing loops where allocation matters, the lower-level read(char[]) is what you want. Second, an empty line is "" (an empty string), not null — the file ends only when readLine() returns null.
BufferedWriter.newLine()
The mirror convenience on the write side:
void newLine() throws IOException; // platform line separator: \n on Unix, \r\n on WindowsnewLine() writes whatever the JVM thinks the current platform's line separator is. That's a feature if you're producing files for human eyes on the local machine; it's a bug if you're producing data files, log files, or anything destined for another machine. The internet runs on \n. Always write \n explicitly when the output needs to be portable:
w.write("line one\n"); // portable
w.newLine(); // platform-dependent: \n on Unix, \r\n on WindowsSame advice for PrintWriter.println and the %n format specifier — they're platform-dependent. Use them only when the output is for local consumption.
The "tail buffer never flushed" trap
This is the bug every Java codebase encounters at least once:
// WRONG
BufferedWriter w = Files.newBufferedWriter(path);
w.write("hello");
return; // 'hello' is sitting in the buffer; nothing on diskA BufferedWriter doesn't push bytes to the OS until either the buffer fills or close() runs. Skip the close and the tail is lost — Files.size(path) is 0 and you have no idea why. The fix is try-with-resources every single time:
try (BufferedWriter w = Files.newBufferedWriter(path)) {
w.write("hello");
} // close() runs here; tail is flushedIf you need the data on disk before the close — a log-tail watcher, or another process polling the file — call flush() explicitly. The buffer doesn't auto-flush after each write; that's the price of having a buffer at all.
Mark and reset
BufferedReader and BufferedInputStream both support a small "look ahead and rewind" API:
in.mark(1024); // remember this position; allow up to 1024 bytes of lookahead
int b = in.read();
in.reset(); // back to the marked positionThis is the only java.io API that lets you read a byte/char and then put it back. It's the foundation of "peek at the first few bytes to figure out the format" code — UTF-8 BOM detection, magic-number sniffing, parser hand-off. Without buffering you can't do it: the raw streams don't have the bytes any more once they've been read.
When buffering doesn't help
Two cases where adding a Buffered* decorator buys nothing:
- The source is already in memory.
ByteArrayInputStreamandStringReaderalready serveread()from abyte[]/Stringin memory; there are no syscalls to amortise. - You're using
Files.readString,Files.readAllBytes,Files.write, ortransferTo. Those calls do their own block-at-a-time I/O with a large internal buffer. Wrapping them inBufferedInputStreamis redundant — the JDK already buffered.
The case where buffering helps is the original one: you're reading or writing small chunks (a single byte, a single line, a printf call) and the source/sink is a real file, socket, or pipe.
A worked example: same load, with and without
The program below copies the same 32 KB blob byte-by-byte from one temp file to another — once with raw FileInputStream/FileOutputStream, once with BufferedInputStream/BufferedOutputStream, once with transferTo for reference. The wall-clock prints make the cost of the missing buffer visible. The example then reads the file's lines through a BufferedReader and demonstrates the "forgot to flush" trap on the write side.
What to take from the run:
- The raw byte-at-a-time copy was orders of magnitude slower than the buffered one. The loop body was identical; the only change was wrapping the file streams in
BufferedInputStream/BufferedOutputStream. That's the entire reason these decorators exist — same API, vastly fewer syscalls. transferTowas as fast as the buffered version (or faster). For "copy bytes from A to B with no transformation,"transferTois what you want — it already buffers internally and the JDK has tuned the loop. Reach for it before writing your own.Files.newBufferedReaderreturned aBufferedReaderdirectly. Notice we never wrotenew BufferedReader(new InputStreamReader(new FileInputStream(...), UTF_8))— that three-deep stack is what the factory hides.readLine()came out of that stack for free.- The leaky writer printed
0 bytesbeforeflush(). Those characters were in the in-memory buffer, not on disk. Callingflush()pushed them out; without the explicit flush (or a properclose()), they'd be lost. This is whytry-with-resources around buffered writers isn't optional — it's the contract that makes the write visible. - The
BufferedReader.readLine()loop is the most common text-processing shape in Java. Memorise thewhile ((line = r.readLine()) != null)form: the assignment-in-condition is idiomatic here, and thenull(not an empty string) sentinel is the loop's end condition.
What's next
Buffering solves the syscall-per-call cost but doesn't change what the bytes mean. The next chapter, Java DataInput and DataOutput Streams, covers the decorators that read and write Java primitives in a portable binary format — the layer that lets you write an int to a file and read it back as an int on a different OS.
Practice
What happens to the data written by `w.write('hello')` if you forget to close a `BufferedWriter` (and never call `flush()`)?