Buffered Streams
Buffered streams wrap an underlying unbuffered stream with an in-memory buffer, dramatically reducing the number of native I/O system calls by batching reads and writes. Without buffering, each read() or write() call typically results in one OS system call, which transitions the CPU between user mode and kernel mode — an operation that costs thousands of CPU cycles. With buffering, data is read from or written to the OS in large chunks (typically 8192 bytes by default), and individual application read() and write() calls are served from memory, requiring no system call unless the buffer fills or empties. Java provides four buffered stream classes: BufferedInputStream and BufferedOutputStream for byte-level I/O, and BufferedReader and BufferedWriter for character-level I/O. All four wrap an existing stream, are transparent to the application (the same read/write API), and dramatically improve performance for I/O-intensive code. This entry covers the internal buffer mechanics, buffer sizing guidance, the flush contract (when buffered data is actually written), the decorator pattern that makes buffering composable, mark/reset functionality in BufferedInputStream, and the readLine() convenience in BufferedReader.
Buffer Mechanics — How Buffering Reduces System Calls
// ── Performance comparison: unbuffered vs buffered ────────────────────
import java.io.*;
import java.nio.file.*;
// Writing 1MB one byte at a time WITHOUT buffering:
long start = System.nanoTime();
try (FileOutputStream fos = new FileOutputStream("test.bin")) {
for (int i = 0; i < 1_000_000; i++) {
fos.write(i & 0xFF); // 1,000,000 system calls — catastrophic
}
}
System.out.printf("Unbuffered: %.0fms%n", (System.nanoTime() - start) / 1e6);
// Typical: ~5000ms on SSD, much longer on HDD
// Writing 1MB one byte at a time WITH buffering:
start = System.nanoTime();
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("test_buffered.bin"))) {
for (int i = 0; i < 1_000_000; i++) {
bos.write(i & 0xFF); // bytes go to 8192-byte buffer → ~122 system calls total
}
} // close() flushes remaining buffer — final system call
System.out.printf("Buffered: %.0fms%n", (System.nanoTime() - start) / 1e6);
// Typical: ~5ms — ~1000x faster
// ── Default buffer size ───────────────────────────────────────────────
BufferedInputStream defaultBIS = new BufferedInputStream(new FileInputStream("test.bin"));
// Buffer is 8192 bytes — not exposed by public API, but documented in source
// ── Custom buffer size ────────────────────────────────────────────────
// 64KB buffer for large sequential file reads:
int BUFFER_SIZE = 65_536;
try (BufferedInputStream bigBuffer = new BufferedInputStream(
new FileInputStream("large.bin"), BUFFER_SIZE)) {
byte[] chunk = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = bigBuffer.read(chunk)) != -1) {
process(chunk, bytesRead);
}
}
// Small buffer (1KB) for many small streams where memory is constrained:
try (BufferedInputStream small = new BufferedInputStream(
new FileInputStream("small.txt"), 1024)) {
// works correctly — just refills buffer more often
}
// ── System call visualization ─────────────────────────────────────────
// File: 100,000 bytes
// Unbuffered read(): 100,000 system calls (read 1 byte each)
// Buffered read(): ~13 system calls (read 8192 bytes each = ceil(100000/8192))
// Manual bulk read(): 1 system call (read(byte[100000]) — one OS call)
// ── The decorator pattern: stacking buffering onto any stream ──────────
// BufferedInputStream wraps ANY InputStream:
InputStream network = socket.getInputStream();
BufferedInputStream bufferedNetwork = new BufferedInputStream(network);
// BufferedOutputStream wraps ANY OutputStream:
OutputStream compressor = new GZIPOutputStream(new FileOutputStream("out.gz"));
BufferedOutputStream bufferedCompressor = new BufferedOutputStream(compressor);
// Writing to bufferedCompressor: buffered → GZIP compressed → fileThe Flush Contract and Stream Closing
// ── Flush contract: data is NOT written until flush or close ─────────
File file = new File("output.txt");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
bos.write("Hello".getBytes());
// At this point, "Hello" is in the 8192-byte buffer — NOT on disk
// If the process crashes here, the file is empty
bos.flush(); // now "Hello" is handed to the OS — visible to other processes
bos.write(" World".getBytes());
bos.close(); // flush() + close on underlying stream — " World" now on disk
// ── try-with-resources: guaranteed flush and close ────────────────────
// CORRECT — always use try-with-resources for streams:
try (BufferedOutputStream safe = new BufferedOutputStream(
new FileOutputStream("safe.bin"))) {
for (int i = 0; i < 100_000; i++) {
safe.write(i & 0xFF);
}
} // close() called automatically — flushes buffer — all 100,000 bytes written
// WRONG — exception before close() loses data:
BufferedOutputStream unsafe = new BufferedOutputStream(
new FileOutputStream("unsafe.bin"));
for (int i = 0; i < 100_000; i++) {
unsafe.write(i & 0xFF);
if (i == 50_000) throw new RuntimeException("Crash!"); // buffer not flushed
}
unsafe.close(); // never reached — last ~50,000 bytes in buffer are lost
// ── Stacked streams: close/flush propagates through the chain ─────────
try (BufferedOutputStream bos2 = new BufferedOutputStream(
new GZIPOutputStream(
new FileOutputStream("data.gz")))) {
bos2.write(largeData);
}
// close() chain: BufferedOutputStream.close()
// → flush buffer to GZIPOutputStream
// → GZIPOutputStream.close() (writes GZIP trailer)
// → FileOutputStream.close() (closes file descriptor)
// ── flush() for network protocols ─────────────────────────────────────
try (Socket socket = new Socket("api.example.com", 80);
BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream())) {
String request = "GET / HTTP/1.0
Host: api.example.com
";
out.write(request.getBytes());
out.flush(); // critical: without this, the server never receives the request
// — it's still in the 8192-byte buffer on our end
// Now read the response...
}
// ── sync() for durability guarantees ─────────────────────────────────
try (FileOutputStream fos = new FileOutputStream("critical.dat");
BufferedOutputStream bos3 = new BufferedOutputStream(fos)) {
bos3.write(criticalData);
bos3.flush(); // hand data to OS kernel buffer
fos.getFD().sync(); // force OS to write kernel buffer to physical disk
// After sync(): data survives a power failure
}mark/reset in BufferedInputStream and Composition Patterns
// ── mark/reset in BufferedInputStream ────────────────────────────────
byte[] data = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; // PNG header
try (BufferedInputStream bis = new BufferedInputStream(
new ByteArrayInputStream(data))) {
// Check if markSupported() before using:
System.out.println("Mark supported: " + bis.markSupported()); // true
bis.mark(8); // mark position 0, can read 8 bytes before mark invalid
byte[] header = new byte[4];
bis.read(header); // read first 4 bytes: [0x89, 0x50, 0x4E, 0x47]
String format = detectFormat(header); // e.g., "PNG"
System.out.println("Detected format: " + format);
bis.reset(); // return to mark position — position is 0 again
// Now re-read with proper parser for the detected format:
if ("PNG".equals(format)) {
parsePng(bis); // reads from position 0 again
}
}
// ── Format detection with mark/reset ─────────────────────────────────
public static String detectFormat(BufferedInputStream bis) throws IOException {
if (!bis.markSupported()) throw new IllegalArgumentException("Stream must support mark");
bis.mark(16); // allow reading 16 bytes for detection
byte[] magic = new byte[4];
int read = bis.read(magic);
bis.reset(); // always reset after detection
if (read < 4) return "UNKNOWN";
// PNG: 89 50 4E 47
if (magic[0] == (byte)0x89 && magic[1] == 'P' && magic[2] == 'N' && magic[3] == 'G')
return "PNG";
// JPEG: FF D8 FF
if (magic[0] == (byte)0xFF && magic[1] == (byte)0xD8 && magic[2] == (byte)0xFF)
return "JPEG";
// PDF: %PDF
if (magic[0] == '%' && magic[1] == 'P' && magic[2] == 'D' && magic[3] == 'F')
return "PDF";
return "UNKNOWN";
}
// ── Standard composition patterns ────────────────────────────────────
// Byte file I/O — the standard pattern:
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("in.bin"));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("out.bin"))) {
byte[] buffer = new byte[8192];
int n;
while ((n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
}
}
// Character file I/O with explicit encoding — the standard pattern:
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("text.txt"), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("out.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
}
// With compression:
try (BufferedInputStream in = new BufferedInputStream(
new GZIPInputStream(new FileInputStream("data.gz")));
BufferedOutputStream out = new BufferedOutputStream(
new FileOutputStream("decompressed.bin"))) {
byte[] buf = new byte[65536];
int n;
while ((n = in.read(buf)) != -1) out.write(buf, 0, n);
}