Buffered I/O
Buffered streams wrap raw I/O sources with an internal memory buffer, turning many slow small reads/writes into far fewer large, efficient ones.
Every unbuffered read or write to a file, socket, or other OS-backed stream typically involves a system call, and system calls are orders of magnitude slower than in-memory operations. Buffered I/O classes — BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter — sit between your code and a raw stream, accumulating data in an internal array so that most read()/write() calls are served from memory, with the underlying stream touched only occasionally in large chunks.
The Performance Problem Buffering Solves
Consider reading a 1 MB file one byte at a time with a raw FileInputStream. Each call to read() may cross into the operating system to fetch data, and doing that a million times is dramatically slower than doing it in, say, 8 KB chunks 128 times. Buffered streams automate exactly this batching: internally they read a large block into a byte[] or char[] buffer once, then serve individual read() calls from that in-memory array until it is exhausted, at which point they refill it with one more block read.
The performance gap is not specific to any one Java version or JVM — it comes from the fundamental cost of a system call versus an array access, so buffering matters on every platform.
The Decorator Pattern in java.io
Buffered classes are decorators: they wrap another stream of the same abstract type and add buffering behavior without changing the interface. This is why you construct them by passing an existing stream into the constructor, and it is why they compose freely with any concrete stream implementation — file, network socket, byte array, and so on.
Each Buffered* class implements the same abstract type it wraps (InputStream, OutputStream, Reader, or Writer), so callers that only know about the base type don't need to know buffering is happening underneath.
import java.io.*;
// Byte-oriented buffering
InputStream in = new BufferedInputStream(new FileInputStream("data.bin"));
OutputStream out = new BufferedOutputStream(new FileOutputStream("copy.bin"));
// Character-oriented buffering
Reader reader = new BufferedReader(new FileReader("notes.txt"));
Writer writer = new BufferedWriter(new FileWriter("out.txt"));BufferedInputStream and BufferedOutputStream
BufferedInputStream maintains an internal byte[] buffer (default size 8192 bytes, though a larger size can be passed to the constructor) and refills it from the wrapped stream as needed; it also supports mark()/reset() so callers can 're-read' recently seen bytes without hitting the source again, useful for lookahead parsing. BufferedOutputStream accumulates written bytes in its buffer and only calls the wrapped stream's write() when the buffer fills or flush()/close() is called.
Even though this code reads/writes in 8192-byte application-level chunks already, wrapping with Buffered* still helps for many I/O patterns (e.g. single-byte reads elsewhere, or writes smaller than the buffer) and costs nothing extra to include.
import java.io.*;
public class CopyBinary {
public static void main(String[] args) throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream("source.bin"));
OutputStream out = new BufferedOutputStream(new FileOutputStream("dest.bin"))) {
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
} // close() flushes automatically
}
}BufferedReader and BufferedWriter
BufferedReader and BufferedWriter add the same buffering strategy for character streams, plus text-oriented convenience methods. BufferedReader.readLine() reads and returns a full line (without the line terminator), returning null at end of stream — one of the most commonly used I/O methods in Java. BufferedWriter adds newLine(), which writes the platform-appropriate line separator.
import java.io.*;
import java.nio.charset.StandardCharsets;
try (BufferedReader br = new BufferedReader(new FileReader("in.txt", StandardCharsets.UTF_8));
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line.toUpperCase());
bw.newLine();
}
}Choosing a Buffer Size
All four classes accept an optional int size parameter in a second constructor overload; omitting it uses a default (commonly 8192 for byte streams, JVM-dependent) that is reasonable for most workloads. Larger buffers can help throughput for sequential bulk transfers of large files, at the cost of more memory per open stream; tiny buffers defeat the purpose of buffering. Benchmarking with realistic data is the only reliable way to pick a non-default size — treat 8 KB-ish defaults as a solid starting point, not a hard rule.
InputStream in = new BufferedInputStream(new FileInputStream("bigfile.dat"), 65536); // 64 KB bufferflush() and close() Semantics
Because output data may sit in the buffer rather than reach the underlying stream immediately, flush() forces any buffered bytes/characters out to the wrapped stream right now. close() implicitly flushes before releasing the underlying resource. Forgetting to flush or close an output stream is a classic bug: the program appears to succeed, but the target file is truncated or empty because buffered data never made it out before the process ended or the stream reference was dropped.
try-with-resources is the idiomatic way to guarantee close() (and therefore flush()) runs, even when an exception is thrown mid-write.
mark() and reset() for Lookahead
BufferedInputStream and BufferedReader support mark(readAheadLimit) to remember the current position, and reset() to rewind to it, as long as no more than readAheadLimit bytes/characters have been read since the mark. This enables simple lookahead parsing — peek ahead, and if the data doesn't match what you expected, rewind and try a different interpretation — without needing a full custom buffering scheme.
markSupported() reports whether a given stream supports this; not all Reader/InputStream implementations do, but the Buffered* classes always do.
import java.io.*;
BufferedReader br = new BufferedReader(new StringReader("HELLO world"));
br.mark(5);
char[] peek = new char[5];
br.read(peek);
System.out.println(new String(peek)); // HELLO
br.reset();
char[] again = new char[5];
br.read(again);
System.out.println(new String(again)); // HELLO (re-read from the mark)Buffered I/O and try-with-resources
When you wrap a stream — e.g. new BufferedReader(new FileReader(...)) — only the outermost object needs to be closed explicitly (or listed in try-with-resources); its close() call propagates down and closes the wrapped stream automatically. Closing only the inner stream and leaving the outer BufferedReader referenced risks accessing a wrapper whose underlying resource is already gone, and — more commonly — closing only the inner stream without ever closing the outer one drops any buffered output that was never flushed.
try (BufferedWriter bw = new BufferedWriter(new FileWriter("log.txt"))) {
bw.write("entry");
} // bw.close() flushes buffer AND closes the wrapped FileWriterWhen Buffering Doesn't Help (or Isn't Needed)
Some stream types are already effectively in-memory and gain little from an extra buffering layer — for example, ByteArrayInputStream/ByteArrayOutputStream and StringReader/StringWriter operate entirely on arrays already resident in memory, so wrapping them in Buffered* classes adds overhead without meaningful benefit. Buffering earns its keep specifically around I/O sources that involve system calls: files, sockets, pipes.
Practical Usage: The Standard Pattern
In production code, an unbuffered FileInputStream/FileReader used for anything beyond trivial, tiny files is close to always a mistake. The standard idiom is to wrap immediately at construction time, and modern helpers like java.nio.file.Files.newBufferedReader()/newBufferedWriter() bake this in by returning an already-buffered stream from a single call.
import java.nio.file.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
try (BufferedReader br = Files.newBufferedReader(Path.of("data.csv"), StandardCharsets.UTF_8)) {
String header = br.readLine();
// process header, then remaining lines...
}Mental Model
Picture the raw stream as a slow, one-item-at-a-time conveyor belt from an external source, and the buffer as a bucket standing next to it. A Buffered* class fills the bucket from the belt in large scoops, and your read()/write() calls interact with the bucket, not the belt — the belt is only touched again once the bucket runs dry (on read) or fills up (on write).
Quick Reference
Raw stream Buffered wrapper Adds
FileInputStream -> BufferedInputStream -> mark/reset, fewer syscalls
FileOutputStream -> BufferedOutputStream -> fewer syscalls, flush()
FileReader -> BufferedReader -> readLine(), mark/reset
FileWriter -> BufferedWriter -> newLine(), fewer syscalls
Default buffer size: commonly 8192 (JVM-dependent); override via constructor(int size).Key Takeaways
Buffered I/O classes wrap a raw stream and batch small reads/writes into fewer, larger operations against the underlying resource, dramatically improving performance.
BufferedInputStream/BufferedOutputStream buffer bytes; BufferedReader/BufferedWriter buffer characters and add readLine()/newLine().
Buffered classes follow the decorator pattern: construct by wrapping an existing stream of the same base type.
Closing the outermost buffered stream closes the wrapped stream too and flushes any pending output — close only the outermost wrapper.
flush() (or close()) must run before buffered output is guaranteed to reach its destination; forgetting this can silently lose data.
Buffering helps most for I/O involving system calls (files, sockets); it adds little value over already in-memory streams like ByteArrayInputStream.
FileReader fr = new FileReader("big.txt"); int c; while ((c = fr.read()) != -1) { /* process one char */ } try (BufferedReader br = new BufferedReader(new FileReader("big.txt"))) { int c; while ((c = br.read()) != -1) { /* process one char */ } }
Reading character-by-character (or byte-by-byte) directly from an unbuffered stream is dramatically slower than wrapping it in a Buffered* class first.
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt")); bw.write("data"); // program ends without close() or flush() try (BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"))) { bw.write("data"); }
Buffered output sitting in memory is not guaranteed to reach the file until flush() or close() runs; omitting both can leave the file empty or truncated.
FileInputStream fis = new FileInputStream("f.bin"); BufferedInputStream bis = new BufferedInputStream(fis); //... use bis... fis.close(); // closed the wrong (inner) stream try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("f.bin"))) { //... use bis... } // closes bis, which closes the wrapped FileInputStream too
Only the outermost wrapper needs to be closed; closing the inner stream directly and continuing to use (or separately close) the outer wrapper is redundant and error-prone.
BufferedReader br = new BufferedReader(new StringReader("x")); // pointless wrapping
StringReader sr = new StringReader("x"); // already in-memory; buffering adds no real benefit
Wrapping an already in-memory stream (StringReader, ByteArrayInputStream) in a Buffered* class adds overhead without the I/O savings buffering exists for.
Why is unbuffered file I/O slow for character-by-character or byte-by-byte reads?
Each read() call can involve a system call to the OS, which is much more expensive than an in-memory array access; doing this once per byte/char multiplies that overhead enormously.
What design pattern do BufferedInputStream, BufferedOutputStream, BufferedReader, and BufferedWriter follow?
The decorator pattern — each wraps an existing stream of the same abstract type and adds buffering behavior transparently, without changing the type callers interact with.
If you wrap a FileWriter in a BufferedWriter, which one should you close?
Only the outermost BufferedWriter — its close() call flushes the buffer and propagates down to close the wrapped FileWriter automatically.
What happens if a program writes through a BufferedOutputStream and exits without calling flush() or close()?
Data still sitting in the internal buffer may never reach the underlying file, resulting in missing or truncated output.
What extra capability does BufferedReader provide over a plain FileReader?
readLine() (returns a full line as a String, or null at EOF) and mark()/reset() support for lookahead, in addition to buffered performance.
When does wrapping a stream in a Buffered* class provide little or no benefit?
When the underlying stream is already backed by memory, such as StringReader or ByteArrayInputStream, since there's no slow system call to batch away.
How do you choose a non-default buffer size, and why not always use a very large one?
Pass an int size to the constructor; benchmark with realistic data, since larger buffers use more memory per open stream and don't always improve throughput proportionally.
Write a program that copies a text file to another file using BufferedReader/BufferedWriter, preserving line breaks.
import java.io.*; import java.nio.charset.StandardCharsets; public class CopyText { public static void main(String[] args) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader("in.txt", StandardCharsets.UTF_8)); BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null) { bw.write(line); bw.newLine(); } } } } readLine() strips the line terminator, so newLine() is called explicitly after each write() to restore line breaks in the output file.
What does this print?
abc-abc — mark(3) remembers the position before reading, reset() rewinds back to it, so the second read() re-reads the same three characters.
Write a program that benchmarks reading a file byte-by-byte with a raw FileInputStream versus a BufferedInputStream, printing the elapsed time for each.
import java.io.*; import java.nio.file.*; public class BufferBenchmark { public static void main(String[] args) throws IOException { Path path = Path.of("bigfile.dat"); long t0 = System.nanoTime(); try (InputStream in = new FileInputStream(path.toFile())) { while (in.read() != -1) { /* consume */ } } long unbuffered = System.nanoTime() - t0; long t1 = System.nanoTime(); try (InputStream in = new BufferedInputStream(new FileInputStream(path.toFile()))) { while (in.read() != -1) { /* consume */ } } long buffered = System.nanoTime() - t1; System.out.println("Unbuffered ms: " + unbuffered / 1_000_000); System.out.println("Buffered ms: " + buffered / 1_000_000); } }
Both loops read one byte at a time via the same read() call, but the buffered version serves almost all of those calls from an in-memory array, so it is expected to run substantially faster on any reasonably sized file.
BufferedReader br = new BufferedReader(new StringReader("abcdef"));
br.mark(3);
char[] a = new char[3];
br.read(a);
br.reset();
char[] b = new char[3];
br.read(b);
System.out.println(new String(a) + "-" + new String(b));