☕ Java

BufferedReader

BufferedReader wraps any Reader with an in-memory character buffer, dramatically reducing system calls for character-by-character or line-by-line reading. Its defining method is readLine(), which reads a complete line of text terminated by \n, \r, or \r\n and returns it without the terminator, or returns null at end-of-file. Beyond buffering, BufferedReader also exposes a lines() method (Java 8+) that returns a lazy Stream<String> of lines, enabling the full Stream API for file processing without loading the entire file into memory. BufferedReader supports mark/reset with a caller-specified readAheadLimit. It is obtained either by wrapping a Reader (new BufferedReader(new FileReader(...))) or directly from Files.newBufferedReader(path, charset), which is the preferred idiom in modern Java. This entry covers construction and buffer sizing, all read methods and their contracts, readLine() edge cases (empty lines, last line without terminator), the lines() stream and its relationship to reader lifecycle, mark/reset semantics with readAheadLimit, and the use of BufferedReader as a base for protocol parsing.

Construction, Buffer Sizing, and Core Read Methods

BufferedReader is constructed with new BufferedReader(Reader in) (8192-character default buffer) or new BufferedReader(Reader in, int sz) (custom buffer size). The underlying Reader can be any Reader implementation: FileReader, InputStreamReader, StringReader, or any custom Reader. The buffer size determines how many characters are read from the underlying Reader in each fill operation. Larger buffers reduce the number of fill operations but consume more memory. For most file and network I/O, the 8192-character default is appropriate. For large file processing where the bottleneck is I/O bandwidth, buffers of 64KB to 1MB can improve throughput. The read methods mirror those of Reader with the addition of buffering. read() returns the next character as an int (0-65535) or -1 at end of file. read(char[] cbuf, int offset, int len) reads up to len characters into cbuf starting at offset and returns the number actually read, or -1 at EOF. read(char[] cbuf) is equivalent to read(cbuf, 0, cbuf.length). Unlike InputStream, where read() may return 0 bytes for non-blocking streams, Reader.read() always returns at least 1 character or -1 — it may block until data is available but never returns 0. The ready() method returns true if the buffer has characters available or the underlying Reader.ready() returns true. For a BufferedReader over a file, ready() returns true as long as there are characters in the buffer or the file has unread bytes. For network streams, ready() may return false even when data is eventually coming. ready() is an optimization hint, not a check for data availability — it should not be used as a loop condition. skip(long n) skips n characters. This is efficient because it operates on the buffer: if n characters are available in the buffer, they are discarded at memory speed. If more than the buffer holds must be skipped, the buffer is refilled and the process repeats. For skipping to a byte offset in a binary file, use FileInputStream with a FileChannel.position() instead.
Java
// ── Construction ─────────────────────────────────────────────────────
// Standard: wrap FileReader (prefer the Java-11 charset constructor):
BufferedReader br1 = new BufferedReader(
    new FileReader("text.txt", StandardCharsets.UTF_8));

// Custom buffer size: 64KB for large file processing
BufferedReader br2 = new BufferedReader(
    new FileReader("large.txt", StandardCharsets.UTF_8), 65_536);

// From Files.newBufferedReader — the preferred modern idiom:
BufferedReader br3 = Files.newBufferedReader(
    Path.of("text.txt"), StandardCharsets.UTF_8);

// From InputStreamReader for non-file sources (classpath, network, etc.):
InputStream is = HttpURLConnection.openStream(new URL("https://example.com"));
BufferedReader br4 = new BufferedReader(
    new InputStreamReader(is, StandardCharsets.UTF_8));

// ── read() single character ────────────────────────────────────────────
try (BufferedReader br = Files.newBufferedReader(
        Path.of("text.txt"), StandardCharsets.UTF_8)) {
    int ch;
    while ((ch = br.read()) != -1) {
        System.out.print((char) ch);   // cast int to char for output
    }
}

// ── read(char[], offset, len) bulk read ───────────────────────────────
try (BufferedReader br = Files.newBufferedReader(
        Path.of("text.txt"), StandardCharsets.UTF_8)) {
    char[] buffer = new char[4096];
    int charsRead;
    StringBuilder sb = new StringBuilder();
    while ((charsRead = br.read(buffer, 0, buffer.length)) != -1) {
        sb.append(buffer, 0, charsRead);
    }
    String content = sb.toString();
}

// ── skip() skips characters efficiently ──────────────────────────────
try (BufferedReader br = Files.newBufferedReader(
        Path.of("text.txt"), StandardCharsets.UTF_8)) {
    br.skip(100);      // skip first 100 characters
    System.out.println(br.readLine());  // read line 101+ chars in
}

// ── ready() for availability check ───────────────────────────────────
try (BufferedReader br = Files.newBufferedReader(
        Path.of("text.txt"), StandardCharsets.UTF_8)) {
    System.out.println("Ready: " + br.ready());  // true if buffer or file has content
    // Do NOT use as: while (br.ready()) { br.readLine(); } — may stop mid-file
    // if buffer is momentarily empty between fills. Use readLine() != null loop instead.
}

readLine(), Edge Cases, and the lines() Stream

readLine() is the defining method of BufferedReader. It reads characters until a line terminator ( , , or ) or end of file, returns the characters as a String without the terminator, and positions the reader after the terminator. At end of file, readLine() returns null. A key edge case: if the last line of a file does not end with a newline (common in generated files, config files, and some log formats), readLine() returns that line normally — the end of file acts as an implicit line terminator. Empty lines (just a terminator with no content) return an empty String "", not null. The loop idiom is fixed and must always follow the same form: String line; while ((line = br.readLine()) != null) { ... }. Any other loop structure risks infinite loops or missed lines. Checking line.isEmpty() instead of null misses the last line of files that don't end with a newline when the last line is empty — which is exactly when null is returned. The lines() method (Java 8+) returns a Stream<String> that reads lines lazily — each line is read from the reader on demand as the stream is consumed, not all at once. This is memory-efficient for large files. The stream uses the same null-return contract as readLine() to detect EOF. Critical: the Stream returned by lines() holds a reference to the BufferedReader and will throw IOException wrapped in UncheckedIOException if I/O fails during streaming. Also critical: the Stream must be closed (or consumed within a try-with-resources block) to ensure the underlying BufferedReader is closed. The idiomatic pattern: try (Stream<String> lines = br.lines()) { ... } or try (BufferedReader br = ...) { br.lines()... } where the br try-with-resources closes both the reader and its stream. Parallel processing with lines().parallel(): the lines stream supports parallel processing. BufferedReader is not thread-safe, so the Stream framework uses an internal spliterator that reads lines on one thread and distributes them to parallel processors. For truly parallel I/O, reading the entire file into a List<String> first (via Files.readAllLines()) and then parallelizing is often faster because it decouples I/O from computation.
Java
// ── readLine() canonical loop ─────────────────────────────────────────
try (BufferedReader br = Files.newBufferedReader(
        Path.of("text.txt"), StandardCharsets.UTF_8)) {
    String line;
    while ((line = br.readLine()) != null) {   // null = EOF
        System.out.println(line);              // line has NO trailing newline
    }
}

// ── Edge cases ────────────────────────────────────────────────────────
// File: "Hello
World
" (trailing newline)
// readLine() calls: "Hello", "World", null
// Two non-null calls, then null.

// File: "Hello
World" (NO trailing newline)
// readLine() calls: "Hello", "World", null
// Same result — last line without terminator is returned normally.

// File: "


" (three empty lines)
// readLine() calls: "", "", "", null
// Empty strings, not nulls.

// File: "" (empty file)
// readLine() calls: null immediately.

// ── Common WRONG pattern: checking isEmpty instead of null ─────────────
// WRONG: stops at the first empty line
while (!(line = br.readLine()).isEmpty()) { ... }  // NullPointerException at EOF too

// CORRECT:
while ((line = br.readLine()) != null) {
    if (!line.isEmpty()) { /* process non-empty line */ }
}

// ── lines() stream: lazy line-by-line processing ──────────────────────
try (BufferedReader br = Files.newBufferedReader(
        Path.of("access.log"), StandardCharsets.UTF_8)) {
    long errorCount = br.lines()
        .filter(line -> line.contains("ERROR"))
        .count();
    System.out.println("Errors: " + errorCount);
}   // br.close() also closes the stream

// ── lines() with Files.lines shorthand ────────────────────────────────
// Equivalent: Files.lines(path, charset) returns Stream<String> directly
try (Stream<String> lines = Files.lines(
        Path.of("large.log"), StandardCharsets.UTF_8)) {
    lines.filter(l -> l.startsWith("WARN"))
         .map(l -> l.substring(5))
         .forEach(System.out::println);
}   // stream.close() closes the underlying BufferedReader

// ── lines() parallel: decouple I/O from processing ───────────────────
// Sequential I/O into a list, then parallel processing:
List<String> allLines = Files.readAllLines(
    Path.of("data.csv"), StandardCharsets.UTF_8);

long count = allLines.parallelStream()
    .filter(line -> line.contains(",error,"))
    .count();

// ── lines() with UncheckedIOException ────────────────────────────────
try (Stream<String> lines = Files.lines(Path.of("text.txt"))) {
    lines.forEach(line -> {
        // IOException during stream reading wrapped in UncheckedIOException:
        System.out.println(line);
    });
} catch (UncheckedIOException e) {
    System.out.println("I/O error during streaming: " + e.getCause());
}

mark/reset, Protocol Parsing, and Advanced Patterns

BufferedReader supports mark/reset with a caller-specified readAheadLimit. mark(int readAheadLimit) marks the current position; the argument specifies the maximum number of characters that may be read before the mark becomes invalid. Unlike BufferedInputStream where the buffer automatically grows up to readlimit, BufferedReader's behavior when readAheadLimit exceeds the buffer size is implementation-defined — the buffer may or may not be expanded. To guarantee mark stability, specify a readAheadLimit no larger than the buffer size. reset() throws IOException if more than readAheadLimit characters have been read since the mark. Protocol parsing is a common use case for mark/reset in BufferedReader. An HTTP response parser needs to read the status line and headers line by line (using readLine()), then switch to binary mode for the body (which may be chunked or content-length-delimited). Because the body may not be text (it may be a binary file), the reader should not be used for the body — instead, the underlying InputStream should be used. This requires carefully managing the reader/stream boundary. A cleaner approach for mixed text/binary protocols is to use BufferedInputStream for everything and manually decode text headers. Text-binary protocol boundary: when using BufferedReader for the text portion and needing to switch to binary I/O for the body, the reader's buffer may have consumed bytes from the body. This is the fundamental impedance mismatch between character readers and byte streams. The safest pattern is to compute exactly how many bytes the headers consumed and position a byte-level stream accordingly — or to use a protocol library that handles this correctly. The PushbackReader provides a different look-ahead mechanism: unread(int c) pushes a character back so the next read() returns it. PushbackReader is suitable for parser look-ahead of one or a few characters without the stateful mark/reset discipline. PushbackReader wraps any Reader and can be composed with BufferedReader: new PushbackReader(new BufferedReader(new FileReader(...))).
Java
// ── mark/reset with BufferedReader ────────────────────────────────────
try (BufferedReader br = Files.newBufferedReader(
        Path.of("text.txt"), StandardCharsets.UTF_8)) {

    System.out.println("markSupported: " + br.markSupported());   // true

    br.mark(200);   // mark current position; can read up to 200 chars before invalidation
    String first  = br.readLine();
    String second = br.readLine();
    System.out.println("First two lines: " + first + " | " + second);

    br.reset();     // return to marked position
    String again  = br.readLine();
    System.out.println("First line again: " + again);  // same as 'first'
}

// ── Protocol parsing: HTTP response headers ────────────────────────────
public static Map<String, String> parseHttpHeaders(BufferedReader br)
        throws IOException {
    Map<String, String> headers = new LinkedHashMap<>();

    // First line: "HTTP/1.1 200 OK"
    String statusLine = br.readLine();
    if (statusLine == null) throw new IOException("No status line");
    System.out.println("Status: " + statusLine);

    // Headers: "Content-Type: application/json" until blank line
    String header;
    while ((header = br.readLine()) != null && !header.isEmpty()) {
        int colon = header.indexOf(':');
        if (colon > 0) {
            String name  = header.substring(0, colon).trim();
            String value = header.substring(colon + 1).trim();
            headers.put(name, value);
        }
    }
    // After the blank line: body (not read here — caller handles)
    return headers;
}

// ── PushbackReader for single-character look-ahead ─────────────────────
try (PushbackReader pbr = new PushbackReader(
        new BufferedReader(new FileReader("data.txt", StandardCharsets.UTF_8)))) {

    int ch = pbr.read();
    if (ch == '{') {
        pbr.unread(ch);   // push '{' back — next read returns '{' again
        parseJson(pbr);
    } else if (ch == '<') {
        pbr.unread(ch);
        parseXml(pbr);
    }
}

// ── Reading entire file into String efficiently ───────────────────────
// Option 1: BufferedReader + StringBuilder
public static String readAllChars(Path path, Charset charset) throws IOException {
    try (BufferedReader br = Files.newBufferedReader(path, charset)) {
        StringBuilder sb = new StringBuilder();
        char[] buf = new char[8192];
        int n;
        while ((n = br.read(buf)) != -1) {
            sb.append(buf, 0, n);
        }
        return sb.toString();
    }
}

// Option 2: Files.readString (Java 11+) — most concise
String content = Files.readString(Path.of("text.txt"), StandardCharsets.UTF_8);

// Option 3: lines() joined (slightly less efficient — creates list of strings)
String joined = Files.lines(Path.of("text.txt"), StandardCharsets.UTF_8)
    .collect(Collectors.joining("
"));

// ── Counting words in a large file without loading all into memory ────
try (BufferedReader br = Files.newBufferedReader(
        Path.of("novel.txt"), StandardCharsets.UTF_8)) {
    long wordCount = br.lines()
        .flatMap(line -> Arrays.stream(line.split("\s+")))
        .filter(word -> !word.isEmpty())
        .count();
    System.out.println("Words: " + wordCount);
}

Related Topics in Java I/O

FileWriter
FileWriter is a convenience class for writing characters to a file, extending OutputStreamWriter with a FileOutputStream underneath. It encodes Java characters into bytes using the platform's default charset (or an explicit charset since Java 11) and writes them to a named file or File object. FileWriter supports two modes: overwrite (the default, which truncates the file to zero length on opening) and append (which positions the write pointer at the end of the existing file content). Like FileReader, FileWriter is unbuffered — each write() call propagates immediately to the underlying FileOutputStream, triggering system calls. In practice, FileWriter is almost always wrapped in a BufferedWriter to batch writes into efficient OS calls. The charset trap is identical to FileReader: pre-Java-11 constructors use the platform default charset silently, which causes portability problems; Java 11 constructors accept an explicit Charset. This entry covers all constructor variants with their charset and append semantics, the write methods and their character vs string behavior, newLine() in BufferedWriter, the flush/close contract, and the preferred modern alternatives.
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.
PrintWriter
PrintWriter is a character-based output class that wraps any Writer or OutputStream and adds convenience methods for printing formatted representations of all Java primitive types, strings, and objects. Its defining characteristic is that none of its print(), println(), and printf() methods throw checked IOException — errors are silently swallowed and can only be detected after the fact by calling checkError(). This makes PrintWriter easy to use interactively and in situations where I/O failure is genuinely unrecoverable (writing to System.out, generating diagnostic output), but makes it dangerous for critical data writing where exceptions must be caught and handled. PrintWriter can auto-flush on println(), printf(), and format() calls when constructed with autoFlush=true, which is useful for interactive console output and network protocol streams. Its printf() and format() methods delegate to java.util.Formatter, enabling C-style formatted output with full locale awareness. This entry covers all constructor variants and their autoFlush and buffering behavior, every print/println/printf method, the checkError() error detection model, the difference between PrintWriter and PrintStream, charset handling, and when PrintWriter is the right choice versus BufferedWriter.
DataInputStream
DataInputStream wraps any InputStream and adds methods for reading Java primitive types in a machine-independent binary format. It reads boolean, byte, short, int, long, float, double, and char values from the underlying stream using fixed byte widths and big-endian byte order. The big-endian, fixed-width encoding is identical to the format written by DataOutputStream, making the two classes the natural pair for serializing and deserializing primitive data across files, network connections, or inter-process pipes. DataInputStream also provides readFully(), which blocks until exactly the specified number of bytes have been read — filling a buffer completely rather than returning a partial read as InputStream.read() may do. readUTF() reads a string encoded in a modified UTF-8 format (a two-byte length prefix followed by the encoded string bytes) that was written by DataOutputStream.writeUTF(). DataInputStream is unbuffered, so it should always be wrapped inside a BufferedInputStream for performance. This entry covers the full method API, the big-endian byte order contract, readFully() vs read() semantics, the modified UTF-8 format and its limitations, end-of-file detection, and composition patterns for binary protocol parsing.