📁 I/O, Files & Networking · Intermediate

Buffering in Java

BufferedReader/Writer, why unbuffered I/O is slow, flush.

🧩 The mysteryYour program writes a log line, ends normally… and the log file is empty. No error, no exception. Where did the text go?

One trip per item

Imagine carrying groceries from the car one item per trip. That's unbuffered I/O: each read() or write() can become a separate, expensive call into the operating system. A buffer is a shopping bag: it moves data in big chunks (8 KB by default) and serves small reads and writes from memory.

Wrapping streams

Wrap a stream to buffer it: new BufferedReader(reader), BufferedWriter, BufferedInputStream… **BufferedReader also adds readLine()** (returns null at the end) and **lines()**.

try (var in = Files.newBufferedReader(path)) {
    String line;
    while ((line = in.readLine()) != null) {
        System.out.println(line);
    }
}
🔮 Predict it

Counting lines

What does this print?

var br = new BufferedReader(
    new StringReader("x\n\ny\n"));
System.out.println(br.lines().count());
  1. 2
  2. 3
  3. 4
Show the answer

3 — the lines are "x", "" and "y". The empty line in the middle counts, but the final \n doesn't create an extra empty line.

Writers hold your data

A **BufferedWriter collects written text in memory and sends it only when the buffer fills, or when you call flush() or close()**. That's the whole point of buffering — and its catch.

🔮 Predict it

Is it there yet?

What does this print?

void main() throws IOException {
    var sw = new StringWriter();
    var bw = new BufferedWriter(sw);
    bw.write("hey");
    System.out.println("[" + sw + "]");
    bw.flush();
    System.out.println("[" + sw + "]");
}
  1. [hey] [hey]
  2. [] [hey]
  3. [] []
Show the answer

[] — "hey" is still waiting in the BufferedWriter's buffer, so the StringWriter is empty. After **flush()** it arrives: [hey].

⚠️ The trap

The empty log file

The program ends while "started" is still in the buffer, so the file stays empty. **close() flushes automatically — use try-with-resources** so it always happens.

// bug: never flushed or closed
var w = new BufferedWriter(
    new FileWriter("log.txt"));
w.write("started");
 
try (var w2 = new BufferedWriter(
        new FileWriter("log.txt"))) {
    w2.write("started");
} // close() flushes
💼 In the real world

Buffering in production

Reading a big file through an unbuffered stream can be orders of magnitude slower than buffered — a classic performance fix in code reviews. And lost final log lines or truncated CSV exports usually trace back to a writer that was never flushed or closed.

Key takeaways

  1. Wrap streams: new BufferedReader(reader)
  2. BufferedReader adds readLine() and lines()
  3. Written data stays in memory until flush/close
  4. close() flushes automatically — use try-with-resources
🤯 Did you know?

BufferedReader's default buffer is 8,192 characters — you can pass a different size to its constructor if you need to.

Practice questions

Why is reading a file one byte at a time through an unbuffered FileInputStream slow?

  1. Java decodes every byte as UTF-8
  2. Each read() can be a separate, expensive call into the operating system
  3. The file is reopened for every byte
  4. Bytes are read from the end of the file backwards
Check your answer

Each read() can be a separate, expensive call into the operating system. System calls have a large fixed cost. A BufferedInputStream fetches thousands of bytes per call and hands them out from memory.

What does this print?

var br = new BufferedReader(
    new StringReader("one\ntwo\n"));
System.out.println(br.lines().count());
  1. 1
  2. 2
  3. 3
  4. 8
Check your answer

2. lines() splits on line terminators and doesn't produce an extra empty line for the final \n, so there are 2 lines.

Next: bytes become text only through a charset. Why does "é" sometimes turn into "é"?