☕ 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

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.