skip to content

Decorator Pattern & Buffering

java.io composes behavior by wrapping: a BufferedInputStream around a FileInputStream turns many small syscalls into few large ones. It is the canonical Decorator example, and interviewers use it both for I/O and for pattern questions.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

4

What does wrapping an InputStream in a BufferedInputStream actually do, and why does it speed things up?

level: juniorimportance: must knowfreq 70%

answer

  1. 8 KB internal byte[] default
  2. syscall per byte vs syscall per buffer-fill
  3. wrap, don't replace
  4. output must flush/close
  5. no benefit over in-memory sources

basics

~20 s

BufferedInputStream wraps another stream and reads a big chunk of bytes at once into an in-memory array. Your reads come from that array instead of hitting the disk or network each time, so there are far fewer slow system calls.

solid answer

~40 s

A BufferedInputStream is a wrapper around an underlying stream (for example a FileInputStream). On the first read it pulls a large block of bytes (default 8 KB) from the underlying stream into an internal byte array, then serves your subsequent reads from that array in memory. The expensive part of I/O is the system call that crosses into the operating system to touch the disk or socket; reading one byte at a time means one syscall per byte. Buffering amortizes that: one syscall fills the buffer, and the next few thousand reads are cheap array accesses. The same idea applies to BufferedOutputStream (it collects writes and flushes them in bulk) and to BufferedReader/Writer for character streams. You wrap rather than replace, so the buffer composes with whatever source you already have.

code

java · 11 lines
java
// Slow: one syscall per byte
try (InputStream in = new FileInputStream("big.bin")) {
    int b;
    while ((b = in.read()) != -1) { /* ... */ }
}

// Fast: wrap once, reads come from an 8 KB in-memory buffer
try (InputStream in = new BufferedInputStream(new FileInputStream("big.bin"))) {
    int b;
    while ((b = in.read()) != -1) { /* ... */ }
}

go deeper

for a junior

Knows BufferedInputStream reads in chunks and is faster, and that you wrap your file stream in it.

for a middle

Explains the syscall amortization, the default 8 KB buffer, the output flush requirement, and that in-memory sources gain nothing.

for a senior

Quantifies the syscall reduction, contrasts byte vs character buffering, and reasons about when buffering is pointless or even slightly harmful.

for a principal

Frames buffering as one layer in a broader I/O strategy (page cache, NIO channels, buffer sizing relative to block size) and can justify tuning the buffer size to the workload.

## The problem buffering solves A **stream** in `java.io` is an ordered sequence of bytes you read from or write to a source/destination (a file, a network socket, memory). The base class `InputStream` has a method `int read()` that returns the next single byte (0–255), or `-1` at end of stream. The catch: when the real source is a file or a network connection, each actual read requires a **system call** — a transition from your Java code into the operating-system kernel, which then talks to the disk or the network card. A system call is *orders of magnitude* slower than an ordinary in-memory operation (think microseconds vs nanoseconds). If you loop calling `read()` one byte at a time over a 1 MB file, that is roughly a million system calls. That is the bottleneck. ## What a buffer is A **buffer** here is just a plain `byte[]` array living in your program's memory. `BufferedInputStream` holds such an array (default size 8192 bytes = 8 KB). ## How BufferedInputStream works When you call `read()` on a `BufferedInputStream`: 1. If its internal array still has unread bytes, it returns the next one directly from the array — no system call, just an array index. Fast. 2. If the array is empty (or exhausted), it makes **one** system call to the underlying stream to refill the whole array (e.g. read up to 8192 bytes at once), then returns the first byte from it. So a million single-byte `read()` calls become roughly `1_000_000 / 8192 ≈ 123` system calls instead of a million. That is why it is dramatically faster. ## The output side `BufferedOutputStream` does the mirror image: `write(b)` puts the byte into its internal array and returns immediately. Only when the array fills up (or you call `flush()` / `close()`) does it perform one bulk write to the underlying stream. This batches many tiny writes into few large syscalls. Because output is held back, you must **flush** (or close, which flushes) to be sure buffered bytes actually reach the destination — otherwise the last partial buffer can be lost. ## Characters vs bytes There is a parallel family for *characters*: `Reader`/`Writer`. `BufferedReader` buffers characters and adds the convenient `readLine()` method; `BufferedWriter` buffers character writes. Same rationale. ## Why "wrap" You do not replace your source stream; you pass it into the buffered stream's constructor: `new BufferedInputStream(new FileInputStream(f))`. The buffered stream *decorates* the file stream — it adds buffering behavior while still reading from the same file. This composition is the decorator pattern (covered in a sibling question). ## When buffering does NOT help If the underlying stream is already in memory (e.g. `ByteArrayInputStream`), there is no syscall to amortize, so wrapping it in a buffer adds overhead for no benefit. Buffering helps when the source/sink is slow (disk, network).

  • Why is reading one byte at a time from a FileInputStream so slow?
    Each read() crosses into the OS kernel as a system call to touch the disk; syscalls are far slower than memory access, so doing one per byte dominates the runtime.
  • What happens if you forget to flush a BufferedOutputStream?
    Bytes still sitting in the internal array never reach the destination. close() flushes automatically, which is why try-with-resources is the safe idiom.

saying these in an interview costs you the question

  • Thinking buffering makes the data smaller or compresses it
  • Believing it speeds up an already in-memory ByteArrayInputStream
  • Forgetting that BufferedOutputStream needs flush/close or bytes are lost
  • Saying it caches across the program (it is a one-pass forward buffer, not a cache)

context

open as a page

Why is java.io considered a textbook example of the Decorator design pattern? Explain how stream wrapping illustrates it.

level: middleimportance: should knowfreq 62%

basics

~20 s

Each stream class wraps another stream of the same type and adds one behavior (buffering, data conversion, etc.). Because the wrapper has the same interface as what it wraps, you can stack wrappers in any order to combine features without subclassing every combination.

open as a page

Does the order in which you wrap decorator streams matter? Give an example involving buffering, compression, and flushing.

level: seniorimportance: should knowfreq 45%

basics

~20 s

Yes. Each layer sees the bytes the layer below produces, so wrapping order decides whether you buffer raw or compressed data. Put the buffer next to the slow source (the file/socket) so it batches real I/O, and remember to flush the whole chain before relying on the output.

open as a page

When would you reach for java.io decorator streams with buffering versus NIO channels and ByteBuffers? What does each model optimize?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

The java.io decorator streams (with BufferedInputStream/Reader) are simple, blocking, byte/character-at-a-time APIs that are great for straightforward sequential file and text work. NIO channels with ByteBuffers are lower-level and support non-blocking, selectable, and bulk transfers, which scale better for many concurrent connections.

open as a page