☕ Java

FileReader

FileReader is a convenience class for reading character files, extending InputStreamReader with a FileInputStream underneath. It opens a file and decodes its bytes into characters using the platform's default charset (or, since Java 11, an explicitly specified charset). The critical distinction between FileReader and FileInputStream is the abstraction level: FileInputStream reads raw bytes; FileReader reads decoded characters, handling the byte-to-character conversion transparently. Before Java 11, FileReader offered no constructor accepting an explicit charset — it silently used the JVM's default charset, which differs between platforms and locales, making it unsuitable for portable applications. Since Java 11, FileReader(File, Charset) and FileReader(String, Charset) constructors allow explicit charset specification, resolving this issue. FileReader is unbuffered — every read() call ultimately results in a system call — so it is almost always wrapped in a BufferedReader for practical use. This entry covers all constructors and their charset behavior, the read contract including return values and end-of-file, the practical wrapping pattern, and when to prefer InputStreamReader or NIO's Files.newBufferedReader over FileReader.

Constructors, Charset Behavior, and the Pre-Java-11 Trap

FileReader has six constructors split across two generations. The pre-Java-11 constructors — FileReader(String fileName), FileReader(File file), and FileReader(FileDescriptor fd) — all use the platform's default charset (Charset.defaultCharset(), which is typically the system locale's encoding: UTF-8 on Linux and macOS, but Windows-1252 or similar on Windows). Code that reads a UTF-8 file on Windows using these constructors will silently produce garbage characters for any non-ASCII content if the platform default is not UTF-8. Java 11 added FileReader(String fileName, Charset charset) and FileReader(File file, Charset charset), allowing explicit charset specification. The FileDescriptor-based constructor has no charset variant because a file descriptor does not carry charset information. Using the Java 11 constructors makes the charset explicit and removes the platform-dependency. The practical impact of the default charset trap: a file written with UTF-8 encoding (which all modern text editors, most web servers, and most APIs use) will decode incorrectly on Windows JVMs where the platform default is Cp1252. Characters outside the ASCII range (é, ü, 中, €) will appear as replacement characters (?) or incorrect characters. The bug is silent — no exception is thrown. This is one of the most common portability issues in Java I/O code. For new code, the preferred alternatives to FileReader are: Files.newBufferedReader(Path, Charset) (Java 7+, returns a BufferedReader, requires explicit charset, handles the buffering layer automatically) and new InputStreamReader(new FileInputStream(file), charset) (explicit charset, allows wrapping any InputStream). Both approaches make the charset visible at the call site. The FileDescriptor constructor FileReader(FileDescriptor fd) is for advanced use cases such as reading from a FileDescriptor obtained from native code, a JNA/JNI interface, or System.in (which is exposed as FileDescriptor.in). It uses the platform default charset and cannot be changed.
Java
// ── Pre-Java-11 constructors: implicit platform default charset ────────
// DANGEROUS on Windows where default may be Cp1252, not UTF-8:
FileReader legacy1 = new FileReader("file.txt");           // platform default
FileReader legacy2 = new FileReader(new File("file.txt")); // platform default
// Both read UTF-8 files incorrectly on Windows if platform default != UTF-8

// ── Java 11+: explicit charset constructors ────────────────────────────
import java.nio.charset.StandardCharsets;

FileReader utf8Reader = new FileReader("file.txt", StandardCharsets.UTF_8);
FileReader latin1Reader = new FileReader(new File("file.txt"), StandardCharsets.ISO_8859_1);
// Charset is explicit — no platform-dependency

// ── What platform default charset is on this JVM ─────────────────────
System.out.println(Charset.defaultCharset());  // e.g., "UTF-8" or "windows-1252"
// Add -Dfile.encoding=UTF-8 JVM arg to force UTF-8 when platform default is wrong

// ── Reading characters one at a time ─────────────────────────────────
try (FileReader fr = new FileReader("text.txt", StandardCharsets.UTF_8)) {
    int ch;
    while ((ch = fr.read()) != -1) {   // read() returns int: 0-65535 for char, -1 for EOF
        System.out.print((char) ch);   // cast int to char for use
    }
}

// ── Reading into a char array ─────────────────────────────────────────
try (FileReader fr = new FileReader("text.txt", StandardCharsets.UTF_8)) {
    char[] buffer = new char[1024];
    int charsRead;
    StringBuilder sb = new StringBuilder();
    while ((charsRead = fr.read(buffer, 0, buffer.length)) != -1) {
        sb.append(buffer, 0, charsRead);  // append only the chars actually read
    }
    System.out.println(sb.toString());
}

// ── The -1 return and end-of-file contract ────────────────────────────
try (FileReader fr = new FileReader("empty.txt", StandardCharsets.UTF_8)) {
    int ch = fr.read();
    System.out.println(ch);   // -1 immediately — file is empty
}

// ── Preferred alternative 1: Files.newBufferedReader ─────────────────
// Cleaner, always buffered, explicit charset, idiomatic modern Java:
try (BufferedReader br = Files.newBufferedReader(
        Path.of("text.txt"), StandardCharsets.UTF_8)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// ── Preferred alternative 2: InputStreamReader for flexibility ────────
// Allows wrapping any InputStream (network, classpath, etc.):
InputStream is = getClass().getResourceAsStream("/data/config.txt");
try (BufferedReader br = new BufferedReader(
        new InputStreamReader(is, StandardCharsets.UTF_8))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

Practical Usage — Wrapping with BufferedReader

FileReader is never used alone in production code. Its read() method reads one character at a time from the underlying FileInputStream, which issues one system call per character read. Reading a 100KB text file one character at a time makes 100,000 system calls — completely impractical. FileReader should always be wrapped in a BufferedReader, which interjects an 8192-character buffer and reduces system calls to approximately one per 8KB of text. The canonical pattern: BufferedReader br = new BufferedReader(new FileReader(path, charset)) — or, preferably, the Files.newBufferedReader(path, charset) shorthand that is equivalent and more concise. The BufferedReader provides readLine(), which reads a complete line (terminated by , , or ) and returns it without the line terminator. readLine() returns null at end-of-file, providing the natural idiom for line-by-line processing. The ready() method on FileReader (inherited from InputStreamReader) returns true if the stream is guaranteed to be ready to read without blocking. For files, this typically returns true when data is available in the buffer (for BufferedReader) or when the file is open and has bytes remaining. For network streams or pipes, ready() may return false even when data is eventually forthcoming. ready() should not be used as a substitute for checking the return value of read() — it is an optimization hint, not a guarantor of data availability. The skip(long n) method skips n characters. On FileReader, this translates to skipping n decoded characters, not n bytes — for multi-byte encodings like UTF-8, skipping n characters may skip more than n bytes in the underlying file. For precise byte-level seeking, use FileInputStream with a FileChannel directly rather than a character reader.
Java
// ── The mandatory pattern: FileReader wrapped in BufferedReader ────────
try (BufferedReader br = new BufferedReader(
        new FileReader("document.txt", StandardCharsets.UTF_8))) {

    String line;
    int lineNumber = 0;
    while ((line = br.readLine()) != null) {
        lineNumber++;
        System.out.printf("%4d: %s%n", lineNumber, line);
    }
}

// ── readLine() return value contract ─────────────────────────────────
try (BufferedReader br = new BufferedReader(
        new FileReader("data.csv", StandardCharsets.UTF_8))) {

    String line = br.readLine();   // first line (no trailing 
 or 
)
    if (line == null) {
        System.out.println("File is empty");
    } else {
        System.out.println("First line: " + line);
        // line does NOT include the line terminator
    }
}

// ── Line terminators: readLine() handles all three ────────────────────
// 
   (Unix/Linux/macOS)
// 

 (Windows)
// 
   (old Classic Mac OS)
// readLine() handles all three transparently — no need to strip terminators

// ── Processing CSV with BufferedReader wrapping FileReader ─────────────
List<String[]> records = new ArrayList<>();
try (BufferedReader br = new BufferedReader(
        new FileReader("data.csv", StandardCharsets.UTF_8))) {
    String line;
    br.readLine();   // skip header line
    while ((line = br.readLine()) != null) {
        if (!line.isBlank()) {   // skip empty lines
            records.add(line.split(",", -1));  // split on comma, keep trailing empty fields
        }
    }
}

// ── Modern alternative: Files.newBufferedReader ───────────────────────
// Exactly equivalent to new BufferedReader(new FileReader(path, charset))
// but shorter and more idiomatic:
try (BufferedReader br = Files.newBufferedReader(
        Path.of("document.txt"), StandardCharsets.UTF_8)) {
    br.lines()                   // Stream<String> of lines
      .filter(line -> !line.isBlank())
      .map(String::trim)
      .forEach(System.out::println);
}

// ── Stream<String> via lines() — lazy line reading ────────────────────
try (BufferedReader br = Files.newBufferedReader(
        Path.of("large.log"), StandardCharsets.UTF_8)) {
    long errorCount = br.lines()
        .filter(line -> line.contains("ERROR"))
        .count();
    System.out.println("Errors: " + errorCount);
}   // BufferedReader closed when try-with-resources exits — stream also closed

// ── ready() for non-blocking check (rarely needed) ───────────────────
try (BufferedReader br = new BufferedReader(
        new FileReader("file.txt", StandardCharsets.UTF_8))) {
    if (br.ready()) {
        System.out.println("Data available: " + br.readLine());
    }
    // ready() on a file with BufferedReader: true if buffer has data or file has more
    // NOT equivalent to "has more lines" — don't use as loop condition
}

Related Topics in Java I/O

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.
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.