Key Takeaways
FileReaderopens a file and decodes bytes into characters;BufferedReaderadds character buffering and line reading.Scanneris a regex-based token parser. Choose it for typed values and configurable delimiters, not simply to read lines.- Specify the file's encoding explicitly.
FileReaderhas accepted aCharsetsince Java 11. - Use try-with-resources, give each input one owner, and compare equivalent parsing workloads before making performance claims.
FileReader vs BufferedReader vs Scanner is a choice between decoding, buffered reading, and parsing—not three interchangeable ways to do the same job. Use BufferedReader for line-oriented input, Scanner for convenient token conversion, and FileReader when you need a file-backed character reader to use directly or wrap.
FileReader vs BufferedReader vs Scanner at a Glance
| Class | Main responsibility | Typical operations | Best fit |
|---|---|---|---|
FileReader | Open a file and decode its bytes | read(), read(char[]) | A file-backed character source |
BufferedReader | Buffer an existing reader and read lines | read(), readLine() | Line-oriented processing with explicit parsing |
Scanner | Find tokens and convert their values | next(), nextInt(), nextDouble() | Simple typed input and delimiter-based formats |
The important relationship is composition: a BufferedReader can wrap a FileReader or another Reader. A Scanner can also accept a reader directly; adding a BufferedReader underneath it isn't required for correctness.
Byte Streams, Character Streams, and Buffering
Byte Streams: Preserve the Raw Data
InputStream and OutputStream are the base classes for byte input and output. Use byte streams for binary content such as images, compressed files, or audio. A text file also contains bytes, but those bytes must be decoded using the correct character encoding before you treat them as text.
Character Streams: Work with Decoded Text
Reader and Writer are the character-stream abstractions. A reader doesn't necessarily decode bytes: for example, its characters might already exist in memory. InputStreamReader specifically bridges the two worlds by decoding bytes from an InputStream into characters; FileReader is its file-oriented subclass.
Buffered Streams: Reuse Data Already Read into Memory
A buffered wrapper keeps data in an application-side memory buffer. Small reads can then be served from that buffer, reducing calls to the underlying reader or stream. This can improve throughput, but it doesn't mean that every unbuffered character read causes a physical disk operation: InputStreamReader itself may read bytes ahead.
BufferedInputStream and BufferedOutputStream buffer bytes; BufferedReader and BufferedWriter buffer characters. These wrappers add capabilities to an existing source or destination rather than replacing its job.
FileInputStream --> InputStreamReader --> BufferedReader --> lines
bytes charset decoder character buffer
Reader --------------------------------> Scanner --------> typed tokens
token parserThe layers answer different questions: where does the data come from, how are bytes decoded, and how will your application consume the resulting text? This wrapper model is the basis of Java's I/O decoration pattern.
FileReader vs BufferedReader: Read Files One Line at a Time
FileReader doesn't provide readLine(); BufferedReader does. Wrapping a file reader adds line-oriented access and a character buffer without changing which file supplies the data. For new code that already uses Path, I'd usually start with Files.newBufferedReader instead of constructing the layers manually.
Create a UTF-8 file named data.txt in your working directory. Save each complete example in a file matching its class name; the examples use Java 11-compatible APIs.
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadLines {
public static void main(String[] args) throws IOException {
Path path = Path.of("data.txt");
try (BufferedReader reader =
Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}readLine() returns the line without its terminator, or null when no characters remain at end-of-file. An empty line is an empty string, not EOF. The try-with-resources block closes the reader even if reading fails.
BufferedReader vs InputStreamReader: Buffering Is Not Decoding
InputStreamReader chooses how bytes become characters. BufferedReader adds buffering and line access to those characters. Use the former when your input arrives as a byte stream, then wrap it if line reading is useful.
The following program demonstrates two alternative constructions. It deliberately reads the same file twice: once through FileReader, then through an explicit byte-stream bridge.
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class ReaderLayers {
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(
new FileReader("data.txt", StandardCharsets.UTF_8))) {
printLines(reader);
}
try (FileInputStream bytes = new FileInputStream("data.txt");
BufferedReader reader = new BufferedReader(
new InputStreamReader(bytes, StandardCharsets.UTF_8))) {
printLines(reader);
}
}
private static void printLines(BufferedReader reader) throws IOException {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}Both FileReader(String, Charset) and FileReader(File, Charset) have been available since Java 11. Explicit encoding is therefore not exclusive to InputStreamReader.
BufferedReader vs Scanner: Lines or Typed Tokens?
A token is a piece of input separated by a delimiter. Scanner uses regular expressions to recognize delimiters and parse values, with whitespace as its default delimiter. Choose it when methods such as nextInt() or nextDouble() remove useful parsing work from your code.
For example, create a UTF-8 numbers.txt containing:
1.5; 2.75
3.0import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Scanner;
public class ScanNumbers {
public static void main(String[] args) throws IOException {
try (Scanner scanner = new Scanner(
Path.of("numbers.txt"), StandardCharsets.UTF_8)) {
scanner.useDelimiter("[;\\s]+");
scanner.useLocale(Locale.US);
double total = 0;
while (scanner.hasNext()) {
if (!scanner.hasNextDouble()) {
throw new IllegalArgumentException(
"Expected a number, got: " + scanner.next());
}
total += scanner.nextDouble();
}
IOException failure = scanner.ioException();
if (failure != null) {
throw failure;
}
System.out.println(total);
}
}
}This example prints 7.25. Its delimiter treats runs of semicolons or regex whitespace as separators, and Locale.US makes the expected decimal-point convention explicit. Locale matters because decimal and grouping symbols affect numeric parsing.
For format-specific file processing, see our examples of reading a CSV file in Java.
Scanner Failures and Blocking Behavior
next()and numeric next-style methods can throwNoSuchElementExceptionwhen input is exhausted.- Numeric conversions can throw
InputMismatchExceptionfor an invalid token; integer conversions also reject out-of-range values. - A mismatched token isn't consumed automatically. Reject it or explicitly consume it before retrying.
hasNext()andnext()may block waiting for input. A check isn't a nonblocking readiness guarantee.
These behaviors matter particularly for interactive input, where the next delimiter or EOF may not have arrived yet.
Scanner treats an underlying read IOException as end-of-input and makes the exception available through ioException(). The example checks that method before reporting success, rather than silently accepting a partially read file.
Performance: Compare Equivalent Work
For a high-volume import, I'd start with BufferedReader plus targeted parsing, then measure. Scanner's delimiter and numeric-pattern matching adds work beyond reading lines; that's a reason to expect overhead, not evidence of a universal speed ratio. Benchmark equivalent parsing and validation—not Scanner.nextInt() against an unparsed readLine().
Likewise, buffering isn't a guaranteed speedup. Its benefit depends on your read sizes, underlying source, and existing buffering. Start with the standard buffer rather than tuning its size without measurements.
Modern Alternatives: Files.readString and Files.lines
For a file that comfortably fits in memory, Files.readString(Path, Charset), available since Java 11, is concise and closes the file internally. For incremental line processing, Files.lines(Path, Charset) returns a lazy stream that you must close.
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class FileOptions {
public static void main(String[] args) throws IOException {
Path path = Path.of("data.txt");
// Whole-file reading: only for suitably sized input.
String text = Files.readString(path, StandardCharsets.UTF_8);
System.out.print(text);
// Separate example: process lines without retaining the whole file.
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
System.out.println("Line count: " + lines.count());
}
}
}The no-charset overloads of Files.readString, Files.lines, and Files.newBufferedReader specifically use UTF-8, independently of the JVM default charset. With Files.lines, opening failures are reported as IOException; subsequent reading failures are wrapped in UncheckedIOException.
Resource Ownership and Thread Safety
Use try-with-resources for file-backed readers and scanners. Closing a BufferedReader closes its wrapped reader; closing a Scanner closes its input source if that source implements Closeable. Don't close a wrapper around shared input, such as System.in, unless you own that input's lifetime.
Scanner isn't safe for concurrent use without external synchronization. In OpenJDK, BufferedReader synchronizes individual reading operations on its lock, but that doesn't make a multi-step workflow atomic.
Prefer one input owner that hands completed lines or records to workers. If sharing is unavoidable, protect the entire workflow with coordinated external synchronization. Our Java multithreading examples cover the surrounding concurrency patterns.
Common Mistakes
- Calling FileReader completely unbuffered. It lacks
BufferedReader's character-buffer and line-reading API, but its decoding layer may read bytes ahead. - Reading from both a wrapper and its source. Once you've wrapped a reader, consume input through the wrapper; read-ahead makes mixed access unreliable.
- Mixing token and line reads unintentionally. After
nextInt(),nextLine()returns the remainder of the current line, which may be empty—not necessarily the next logical answer. - Assuming streaming guarantees bounded memory. A single huge line or token can still grow in memory. Set appropriate input-size limits when processing untrusted data.
FAQ
What is the difference between FileReader and BufferedReader?
FileReader opens a file and decodes its bytes into characters. BufferedReader wraps a Reader to add character buffering and readLine(), so you can use the two together.
When should I prefer Scanner over BufferedReader?
Prefer Scanner when delimiter-based tokenization and typed conversions make your input handling simpler. Prefer BufferedReader when you primarily need lines or want explicit control over parsing for high-volume input.
How do I read a UTF-8 text file in Java?
Use Files.newBufferedReader(path, StandardCharsets.UTF_8) for line-oriented reading, and close the reader with try-with-resources. Files.readString(path, StandardCharsets.UTF_8) is convenient when the whole file comfortably fits in memory.
Is FileReader buffered?
FileReader's decoding layer may read bytes ahead, so calling it completely unbuffered is misleading. It doesn't provide BufferedReader's character-buffer and readLine() API.
Is BufferedReader thread-safe?
OpenJDK's BufferedReader synchronizes individual reading operations, but compound workflows still need coordination. Prefer a single input owner or external synchronization; Scanner also requires external synchronization for concurrent use.
Further Reading
- FileReader API — inheritance, constructors, and charset selection.
- BufferedReader API — buffering, line reading, and wrapper usage.
- InputStreamReader API — byte-to-character decoding and read-ahead.
- Scanner API — delimiters, numeric parsing, blocking, and exceptions.
- Files API — buffered readers, whole-file reading, and lazy line streams.
- JDK migration guide: default charset — UTF-8 defaults and compatibility configuration.
- Decorating I/O streams — how Java's reader and stream layers fit together.
- Try-with-resources — automatic cleanup on normal and exceptional exits.
