☕ Java

BufferedWriter

BufferedWriter wraps any Writer with an in-memory character buffer, reducing system calls by accumulating characters until the buffer fills, flush() is called, or close() is called. It adds two capabilities not present in Writer: newLine(), which writes the platform-specific line separator, and an optimized write(String, int, int) that avoids creating a char[] copy by writing directly from the String. BufferedWriter is the standard output partner to BufferedReader — together they provide efficient line-by-line text file processing. It is constructed either by wrapping a Writer (new BufferedWriter(new FileWriter(...))) or via Files.newBufferedWriter(path, charset, options), the modern idiomatic alternative. Like all buffered streams, correct usage requires try-with-resources to guarantee that buffered data is flushed and the file is closed even when exceptions occur. This entry covers construction and buffer sizing, all write methods and their interaction with the buffer, newLine() and its platform behavior, flush semantics including when explicit flush is necessary, the difference between close() and flush(), and performance patterns for high-throughput text writing.

Construction, Write Methods, and newLine()

BufferedWriter is constructed with new BufferedWriter(Writer out) (8192-character default buffer) or new BufferedWriter(Writer out, int sz) (custom buffer size). The underlying Writer can be FileWriter, OutputStreamWriter, StringWriter, or any other Writer. The modern alternative, Files.newBufferedWriter(Path path, Charset charset, OpenOption... options), returns a BufferedWriter wrapping an OutputStreamWriter over a FileOutputStream, handling the entire construction in one line with explicit charset and open options (CREATE, APPEND, TRUNCATE_EXISTING, etc.). BufferedWriter inherits three write methods from Writer and adds one. write(int c) writes one character (the int is treated as a char value). write(String s) writes the entire string. write(String s, int off, int len) writes a substring. write(char[] cbuf, int off, int len) writes a portion of a char array. The key behavioral difference from an unbuffered Writer: these write calls add characters to the buffer rather than immediately encoding and writing to the OS. When the buffer fills, it is automatically flushed to the underlying Writer. newLine() writes System.lineSeparator() — on Unix/macOS, on Windows — to the buffer. It is always preferable to hardcoded for files intended for platform-native consumption. For cross-platform file formats (JSON, XML, HTTP, CSV per RFC 4180), hardcoded is the correct choice because those formats specify line endings independently of the platform. The flush() and close() distinction: flush() writes all buffered characters to the underlying Writer (and recursively through any chain) but leaves the stream open for further writes. close() calls flush() first, then closes the underlying Writer and releases the file descriptor. Both methods throw IOException. After close(), any write or flush call throws IOException. After flush(), the stream continues to accept writes. Custom buffer sizes: 64KB (65536) or 256KB for writing large files where disk I/O bandwidth is the bottleneck — the larger buffer means fewer transitions between writing to the buffer and flushing to the OS. The benefit is measurable for sequential writes of many small strings to large files. For interactive or network protocols where latency matters, the default 8192 or even no buffering (letting the protocol library manage buffering) is more appropriate.
Java
// ── Construction patterns ─────────────────────────────────────────────
// Wrap FileWriter (Java 11+ charset constructor):
BufferedWriter bw1 = new BufferedWriter(
    new FileWriter("output.txt", StandardCharsets.UTF_8));

// Custom 64KB buffer:
BufferedWriter bw2 = new BufferedWriter(
    new FileWriter("large.txt", StandardCharsets.UTF_8), 65_536);

// Modern idiom: Files.newBufferedWriter
BufferedWriter bw3 = Files.newBufferedWriter(
    Path.of("output.txt"), StandardCharsets.UTF_8);   // CREATE + TRUNCATE_EXISTING default

// Append mode:
BufferedWriter bw4 = Files.newBufferedWriter(
    Path.of("log.txt"), StandardCharsets.UTF_8,
    StandardOpenOption.CREATE, StandardOpenOption.APPEND);

// ── Write methods ─────────────────────────────────────────────────────
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("out.txt"), StandardCharsets.UTF_8)) {

    bw.write('H');                        // single character (intchar)
    bw.write(72);                         // same: 'H' (Unicode code point)
    bw.write("Hello, World!");            // entire string → buffer
    bw.write("Hello, World!", 7, 5);      // "World" (offset=7, count=5)

    char[] chars = {'J', 'a', 'v', 'a'};
    bw.write(chars, 0, 4);               // entire char array portion

    bw.newLine();                         // System.lineSeparator() → buffer
}

// ── newLine() vs explicit 
 ──────────────────────────────────────────
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("platform.txt"), StandardCharsets.UTF_8)) {
    bw.write("First line");
    bw.newLine();     // 

 on Windows, 
 on Unix — correct for native tools

    bw.write("Second line");
    bw.write("
");   // always 
 — correct for JSON/CSV/HTTP/cross-platform text
}

// ── High-throughput line writing ──────────────────────────────────────
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("million_lines.txt"), StandardCharsets.UTF_8)) {
    for (int i = 0; i < 1_000_000; i++) {
        bw.write(Integer.toString(i));
        bw.newLine();   // goes to buffer — system call only when buffer fills (~8KB)
    }
}   // close() flushes remaining buffer → all 1M lines written

// ── write(String) vs write(String, off, len) ─────────────────────────
String csv = "id,name,value,timestamp";
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("header.csv"), StandardCharsets.UTF_8)) {
    bw.write(csv);          // writes entire "id,name,value,timestamp"
    bw.write(csv, 3, 4);    // writes "name" (offset=3, count=4)
    // write(String, off, len) avoids substring() allocation — writes directly from String
}

Flush, Close, and try-with-resources Discipline

The flush/close discipline for BufferedWriter is the same as for all buffered output streams, but the consequences of getting it wrong are amplified for text: a partially-flushed file contains valid, readable content up to the flush point and nothing after — there is no corruption indicator. A configuration file, CSV report, or log file that is missing its last N lines due to an unflushed buffer is a subtle and difficult-to-diagnose bug. The mandatory pattern: wrap the outermost Writer in a try-with-resources block. The try-with-resources statement guarantees that close() is called when the block exits, regardless of whether it exits normally, by a return statement, or by an exception. close() always calls flush() before closing. This guarantees that all buffered data is written to the file. A subtle order-of-operations issue: when multiple resources are declared in a single try-with-resources, they are closed in reverse declaration order. For a stack of Writers, the outermost (rightmost in declaration) is closed first, which is correct because flushing propagates inward. For BufferedWriter wrapping OutputStreamWriter wrapping FileOutputStream, declaring them as separate resources: try (FileOutputStream fos = ...; OutputStreamWriter osw = new OutputStreamWriter(fos); BufferedWriter bw = new BufferedWriter(osw)) — bw is closed first (flushes to osw), then osw is closed (flushes to fos), then fos is closed. The chain works correctly. When to call explicit flush(): when writing to a network socket and the protocol requires the receiver to get the data before you read the response (HTTP request, database query, etc.) — call flush() after writing the request so the OS sends it. When writing a log entry and you need it to survive an immediately subsequent crash — call flush() after each critical entry. When implementing a progress reporter that writes to a file and updates are expected to be visible in near-real-time — call flush() after each progress update.
Java
// ── try-with-resources: guaranteed flush and close ───────────────────
// CORRECT: close() is guaranteed even on exception
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("safe.txt"), StandardCharsets.UTF_8)) {
    for (int i = 0; i < 100; i++) {
        bw.write("Entry " + i);
        bw.newLine();
        if (i == 50) throw new RuntimeException("Simulated failure");
    }
}   // close() called → flush() called → file has entries 0-50 (up to the exception)

// WRONG: exception before close() loses buffered data
BufferedWriter bw = Files.newBufferedWriter(
    Path.of("unsafe.txt"), StandardCharsets.UTF_8);
bw.write("Important data");
bw.newLine();
throw new RuntimeException("crash");   // buffer NOT flushed — "Important data" is LOST
bw.close();                            // never reached

// ── Reverse close order with separate resource declarations ───────────
// This ensures flush propagates correctly:
try (FileOutputStream   fos = new FileOutputStream("layered.txt");
     OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
     BufferedWriter     bw  = new BufferedWriter(osw)) {
    bw.write("Layered stream test");
    bw.newLine();
}
// Close order: bw.close() → flushes to osw → osw.close() → flushes to fos → fos.close()

// ── Explicit flush: when to call it ──────────────────────────────────
// Network protocol: flush after sending request
try (Socket socket = new Socket("api.example.com", 80);
     BufferedWriter writer = new BufferedWriter(
         new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII));
     BufferedReader reader = new BufferedReader(
         new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {

    writer.write("GET / HTTP/1.1
");
    writer.write("Host: api.example.com
");
    writer.write("
");
    writer.flush();   // CRITICAL: without flush, server never receives the request

    // Now read response:
    String statusLine = reader.readLine();
    System.out.println("Response: " + statusLine);
}

// Progress logging: flush after each entry
try (BufferedWriter log = Files.newBufferedWriter(
        Path.of("progress.log"), StandardCharsets.UTF_8,
        StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
    for (int step = 0; step < 100; step++) {
        processStep(step);
        log.write(LocalDateTime.now() + " Step " + step + " complete");
        log.newLine();
        log.flush();   // visible immediately — survives crash after this line
    }
}

// ── PrintWriter wrapping BufferedWriter: adds formatted output ────────
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(
        Path.of("report.txt"), StandardCharsets.UTF_8))) {
    pw.printf("Report date: %s%n", LocalDate.now());
    pw.printf("Total records: %,d%n", 1_234_567);
    pw.printf("Average value: %.2f%n", 42.5);
    pw.println("--- End of Report ---");
    // PrintWriter.close() closes the wrapped BufferedWriter → flushes → closes
}

BufferedWriter vs PrintWriter and Performance Patterns

BufferedWriter and PrintWriter serve different needs. BufferedWriter is a low-level buffering wrapper — it provides the buffer, newLine(), and the write() methods. It does not provide formatting. PrintWriter wraps a Writer (often a BufferedWriter) and adds print(), println(), printf(), and format() methods. PrintWriter's print/println methods never throw checked IOException — errors are recorded via an internal error flag checked by checkError(). This "silent error" model makes PrintWriter unsuitable for situations where I/O failures must be handled explicitly, which is most production code. For production code, the pattern is: write programmatic formatting yourself (String.format(), StringBuilder, or template strings) and use BufferedWriter for output. Reserve PrintWriter for quick utilities, scripts, and test code where silent error handling is acceptable. If you need printf-style formatting with proper error handling, use Formatter directly or use String.format() to produce strings and then write those strings to a BufferedWriter. Performance for writing many strings: the single most important optimization for high-throughput text writing is using a single large String.format() or StringBuilder per line rather than many separate write() calls. Each write() call that hits the buffer boundary causes a buffer flush — many small writes increase the probability of hitting boundaries. Building each line in memory first (StringBuilder.append() + newLine()) and writing the whole line in one write(String) call is more efficient. For writing structured data (CSV, JSON, TSV), dedicated libraries (OpenCSV, Jackson, Apache Commons CSV) handle quoting, escaping, and encoding correctly, and typically write to a BufferedWriter or BufferedOutputStream. Using a BufferedWriter as the sink for these libraries is the correct composition.
Java
// ── BufferedWriter vs PrintWriter: error handling ────────────────────
// PrintWriter: silent errors
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(
        Path.of("out.txt"), StandardCharsets.UTF_8))) {
    pw.println("Line 1");
    // If the disk is full, pw.println does NOT throw — error is silent
    if (pw.checkError()) {
        System.out.println("PrintWriter had an error — but data was already lost");
    }
}

// BufferedWriter: explicit exceptions
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("out.txt"), StandardCharsets.UTF_8)) {
    bw.write("Line 1");
    bw.newLine();
    // If the disk is full, bw.write() or bw.newLine() throws IOException
    // Exception propagates immediately — caller handles it
} catch (IOException e) {
    System.out.println("Write failed: " + e.getMessage());   // know exactly when failure occurred
}

// ── High-throughput: build full lines, write once per line ────────────
// SLOW: many small write() calls
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("slow.csv"), StandardCharsets.UTF_8)) {
    bw.write("Name");
    bw.write(",");
    bw.write("Score");
    bw.write(",");
    bw.write("Date");
    bw.newLine();   // 6 write calls per line
}

// FAST: build each line, one write() per line
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("fast.csv"), StandardCharsets.UTF_8)) {
    bw.write("Name,Score,Date");
    bw.newLine();   // 2 write calls per line (write + newLine)

    // For data rows, build line in StringBuilder:
    StringBuilder sb = new StringBuilder(128);
    for (DataRecord record : records) {
        sb.setLength(0);   // reset without new allocation
        sb.append(record.name()).append(',')
          .append(record.score()).append(',')
          .append(record.date());
        bw.write(sb.toString());
        bw.newLine();
    }
}

// ── Writing CSV with proper escaping via Apache Commons CSV ──────────
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("data.csv"), StandardCharsets.UTF_8);
     CSVPrinter csv = new CSVPrinter(bw, CSVFormat.DEFAULT.withHeader("Name","Age","City"))) {
    csv.printRecord("Alice", 30, "New York");
    csv.printRecord("Bob, Jr.", 25, "London");   // comma in name — library handles quoting
    csv.printRecord("Carol", 35, "São Paulo");   // Unicode — UTF-8 handles it
}   // bw.close() called → all records flushed

// ── Writing JSON lines (JSONL) format ────────────────────────────────
ObjectMapper mapper = new ObjectMapper();
try (BufferedWriter bw = Files.newBufferedWriter(
        Path.of("data.jsonl"), StandardCharsets.UTF_8)) {
    for (DataRecord record : records) {
        bw.write(mapper.writeValueAsString(record));  // one JSON object per line
        bw.write("
");   // JSONL uses 
, not platform newline
    }
}

Related Topics in Java I/O

I/O Basics
Java I/O is built on a small set of abstract concepts that underlie every I/O operation in the language: streams, readers, writers, channels, and buffers. A stream is a sequential flow of data — bytes moving from a source to a destination one at a time or in chunks. Java organizes I/O around two fundamental distinctions: byte I/O (reading and writing raw bytes, the universal representation that everything ultimately reduces to) and character I/O (reading and writing text encoded in a specific character set, with automatic encoding and decoding). The original java.io package, introduced in Java 1.0, provides stream-based I/O through four abstract base classes: InputStream, OutputStream, Reader, and Writer. The java.nio package, introduced in Java 1.4, adds a channel-and-buffer model for non-blocking and memory-mapped I/O. The java.nio.file package, introduced in Java 7 as part of NIO.2, provides a modern, comprehensive file system API that supersedes much of java.io.File. This entry covers the conceptual model of streams and their abstract base classes, the decorator pattern that underlies Java I/O class hierarchy, the source-processor-sink taxonomy of stream classes, blocking versus non-blocking I/O, buffering and why it is almost always necessary, the standard I/O streams (System.in, System.out, System.err), and the resource management contract that every I/O class must satisfy.
Byte Streams
Byte streams are the fundamental I/O abstraction in Java for reading and writing raw binary data. InputStream and OutputStream are the abstract base classes for all byte-oriented I/O, and their concrete subclasses cover every byte-level data source and destination: files, byte arrays in memory, network sockets, pipes between threads, and process standard streams. The critical read() contract — returning an int from 0 to 255 for valid bytes and -1 for end-of-stream — is the foundation of all stream-based binary processing. Byte streams do not perform character encoding or decoding; every byte is passed through as-is, making them correct for binary formats (images, audio, archives, serialized data, protocol buffers), and incorrect for text unless the encoding is explicitly managed. This entry covers the complete InputStream and OutputStream APIs, every major concrete byte stream class and its use case, DataInputStream and DataOutputStream for structured binary I/O, the mark/reset mechanism, available() and its correct interpretation, skipping and transferTo, and ObjectInputStream and ObjectOutputStream for Java serialization.
Character Streams
Character streams, represented by the Reader and Writer abstract base classes, handle text data by abstracting away the encoding and decoding between Java's internal char/String representation (UTF-16) and the byte encoding used in files and network connections. Where byte streams treat data as raw octets, character streams treat data as Unicode characters, handling multi-byte sequences transparently according to a specified Charset. InputStreamReader and OutputStreamWriter are the bridge classes that connect byte streams to character streams, applying charset encoding on write and decoding on read. BufferedReader adds line-at-a-time reading via readLine() and multi-character buffering. PrintWriter adds print/println/printf formatting output. StringReader and StringWriter enable in-memory character stream operations on String data. This entry covers the complete Reader and Writer APIs, charset handling and the consequences of using the wrong charset, the complete class hierarchy of character streams with the use case for each, BufferedReader.readLine() semantics and the lines() stream, the bridge classes in depth, character encoding best practices, and the interaction between character streams and Java's String.lines() and Files.readString()/writeString() alternatives.
File Handling
File handling in Java spans two generations of API: the legacy java.io.File class introduced in Java 1.0, and the modern java.nio.file package (NIO.2) introduced in Java 7 with its Path interface, Files utility class, and FileSystem abstraction. The File class represents a file or directory path as an abstract pathname and provides methods for querying metadata, listing directory contents, creating and deleting files, and basic path manipulation. Its limitations — no symbolic link support, inconsistent error reporting (methods return boolean instead of throwing exceptions), no atomic operations, limited metadata access, and performance issues for large directory traversals — motivated the complete redesign in NIO.2. The Path interface and Files class cover all functionality of File with better exception handling, symbolic link support, atomic operations, rich metadata via BasicFileAttributes, efficient directory walking with Files.walk() and Files.walkFileTree(), file watching with WatchService, and a provider model for custom file system implementations. This entry covers the complete File API and its limitations, the NIO.2 Path and Files APIs, directory traversal strategies, file watching, temporary files, and best practices for cross-platform path handling.