Blog

FileReader vs BufferedReader vs Scanner | Java

Key Takeaways

  • FileReader opens a file and decodes bytes into characters; BufferedReader adds character buffering and line reading.
  • Scanner is a regex-based token parser. Choose it for typed values and configurable delimiters, not simply to read lines.
  • Specify the file's encoding explicitly. FileReader has accepted a Charset since 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

ClassMain responsibilityTypical operationsBest fit
FileReaderOpen a file and decode its bytesread(), read(char[])A file-backed character source
BufferedReaderBuffer an existing reader and read linesread(), readLine()Line-oriented processing with explicit parsing
ScannerFind tokens and convert their valuesnext(), 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 parser

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

Advertisement

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.0
import 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 throw NoSuchElementException when input is exhausted.
  • Numeric conversions can throw InputMismatchException for 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() and next() 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.

Advertisement

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

Advertisement

Written by Sam Erickson

Pen name of a software engineer with 20+ years in the industry, from Fortune 500 companies to startups. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me

Advertisement
FileReader vs BufferedReader vs Scanner | Java | StackChief