W3docs

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

ClassWraps
BufferedInputStreamAn InputStream. Adds an internal byte[] buffer.
BufferedOutputStreamAn OutputStream. Adds an internal byte[] buffer.
BufferedReaderA Reader. Adds an internal char[] buffer and the famous readLine() method.
BufferedWriterA 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 stream

readLine 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 Windows

newLine() 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 Windows

Same 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 disk

A 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 flushed

If 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 position

This 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. ByteArrayInputStream and StringReader already serve read() from a byte[]/String in memory; there are no syscalls to amortise.
  • You're using Files.readString, Files.readAllBytes, Files.write, or transferTo. Those calls do their own block-at-a-time I/O with a large internal buffer. Wrapping them in BufferedInputStream is 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.

java— editable, runs on the server

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.
  • transferTo was as fast as the buffered version (or faster). For "copy bytes from A to B with no transformation," transferTo is what you want — it already buffers internally and the JDK has tuned the loop. Reach for it before writing your own.
  • Files.newBufferedReader returned a BufferedReader directly. Notice we never wrote new 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 bytes before flush(). Those characters were in the in-memory buffer, not on disk. Calling flush() pushed them out; without the explicit flush (or a proper close()), they'd be lost. This is why try-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 the while ((line = r.readLine()) != null) form: the assignment-in-condition is idiomatic here, and the null (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

Practice

What happens to the data written by `w.write('hello')` if you forget to close a `BufferedWriter` (and never call `flush()`)?