Why use BufferedReader/BufferedWriter when reading or writing text, and how do you create one correctly?
answer
- Buffer = big chunked reads/writes, fewer syscalls
- readLine() returns null at EOF
- Files.newBufferedReader/Writer default UTF-8
- writer must flush/close or data lost
- try-with-resources; closing outer closes inner
basics
~20 sBuffering 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 sUnbuffered 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 linesimport 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
Knows you wrap a reader in BufferedReader and loop readLine() until null.
Explains buffering reduces system calls, and that writers must be flushed/closed or data is lost.
Chooses Files.newBufferedReader for the UTF-8 default, uses try-with-resources correctly, and reasons about buffer size vs throughput.
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