skip to content

Why use BufferedReader/BufferedWriter when reading or writing text, and how do you create one correctly?

level: middleimportance: must knowfreq 68%

answer

  1. Buffer = big chunked reads/writes, fewer syscalls
  2. readLine() returns null at EOF
  3. Files.newBufferedReader/Writer default UTF-8
  4. writer must flush/close or data lost
  5. try-with-resources; closing outer closes inner

basics

~20 s

Buffering reads or writes data in big chunks instead of one character at a time, which is much faster because it makes far fewer slow disk/OS calls. You wrap a reader/writer, e.g. new BufferedReader(new FileReader(file)), and read line by line with readLine().

solid answer

~40 s

Unbuffered readers and writers hit the operating system on every tiny read or write, and each system call is expensive. A BufferedReader/BufferedWriter keeps an in-memory buffer (default 8 KB), so it fills the buffer with one big OS read and serves your small reads from memory — and batches small writes until the buffer is full. Practically, BufferedReader also gives you readLine(), the standard way to iterate a file line by line without loading it all into memory. You create one by wrapping: new BufferedReader(new FileReader(path, StandardCharsets.UTF_8)), or better Files.newBufferedReader(path) which defaults to UTF-8. Always use try-with-resources so it closes, and remember BufferedWriter must be flushed/closed or buffered data is lost. Closing the outer wrapper closes the underlying stream too.

code

java · 18 lines
java
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;

// Idiomatic line-by-line read (streams, low memory)
try (BufferedReader br = Files.newBufferedReader(Path.of("in.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// Buffered write; close() flushes the buffer
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("out.txt"), StandardCharsets.UTF_8)) {
    bw.write("hello");
    bw.newLine();
}

go deeper

for a junior

Knows you wrap a reader in BufferedReader and loop readLine() until null.

for a middle

Explains buffering reduces system calls, and that writers must be flushed/closed or data is lost.

for a senior

Chooses Files.newBufferedReader for the UTF-8 default, uses try-with-resources correctly, and reasons about buffer size vs throughput.

for a principal

Understands the decorator design, the syscall/throughput trade-off, and sets conventions around streaming vs whole-file and explicit charsets across services.

## Why buffering exists Disk and OS access is *slow* compared to memory. Every call like `read()` on an unbuffered `FileReader` can translate into a separate **system call** (a request into the operating system kernel). Doing that one character at a time for a megabyte file means millions of system calls — extremely slow. A **buffer** is a chunk of memory (default 8192 chars for `BufferedReader`). The idea: read a *big* block from disk into the buffer once, then serve the program's small `read()` calls out of that fast in-memory buffer. When the buffer empties, refill it with another big block. Writing is the mirror image: small writes accumulate in the buffer and are flushed to disk in one big block when full (or on `flush()`/`close()`). ## The decorator pattern Java I/O uses the *decorator* pattern: you wrap one stream in another to add behavior. `BufferedReader` wraps any `Reader`: ```java try (BufferedReader br = new BufferedReader( new FileReader("notes.txt", StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null) { // readLine returns null at EOF process(line); } } ``` `readLine()` is the payoff: it returns one line at a time (without the line terminator) and `null` at end-of-file. This streams the file — only one line is in memory at once — so it works for arbitrarily large files. ## The NIO shortcut ```java try (BufferedReader br = Files.newBufferedReader(Path.of("notes.txt"))) { ... } ``` `Files.newBufferedReader` returns an already-buffered reader and **defaults to UTF-8** — cleaner and avoids the encoding pitfall of bare `FileReader`. ## Writing and flushing ```java try (BufferedWriter bw = Files.newBufferedWriter(Path.of("out.txt"))) { bw.write("line one"); bw.newLine(); // platform line separator bw.write("line two"); } // close() flushes automatically ``` Critical: a `BufferedWriter` holds data in memory until the buffer fills or you `flush()`/`close()`. **If you forget to close it, the last buffered chunk is silently lost.** try-with-resources guarantees `close()` (which flushes). ## try-with-resources and closing chains `try (Resource r = ...)` automatically calls `r.close()` at the end, even on exception. Closing the *outer* decorator (`BufferedReader`) closes the wrapped `FileReader` too, so you only declare the outermost in the try. ## Summary Buffering = fewer, bigger OS calls = speed; `BufferedReader.readLine()` = idiomatic line iteration; always close (try-with-resources); writers must flush/close or you lose data.

  • What does BufferedReader.readLine() return at end of file?
    null. The idiom is while ((line = br.readLine()) != null) { ... }.
  • Why prefer Files.newBufferedReader over new BufferedReader(new FileReader(...))?
    It is already buffered and defaults to UTF-8, avoiding the platform-default charset trap and one layer of wrapping.

saying these in an interview costs you the question

  • Reading char-by-char with an unbuffered FileReader on big files
  • Forgetting to close/flush a BufferedWriter (lost data)
  • Using bare new FileReader without specifying charset (platform-default trap pre-18)
  • Thinking readLine() throws at EOF instead of returning null

context