☕ 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

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.