Reading Files
Reading Files
Java offers several distinct ways to read a file's content, and the right one depends on the file's size, whether it holds text or binary data, and whether the whole thing needs to be in memory at once. This article surveys the main approaches; wrapping a reader in a buffer for performance is covered in depth in this section's next article, BufferedReader & Writer.
What Is the Right Way to Read a File?
There is no single right answer — Java gives four genuinely different approaches, and the right one depends on the file's size, whether it holds text or binary data, and whether the whole thing needs to fit in memory at once. Reading a file character by character with a raw FileReader works, but it takes several lines to accumulate the result into something usable.
1// File: BeforeReadingFiles.java
2import java.io.*;
3import java.nio.file.*;
4
5public class BeforeReadingFiles {
6 public static void main(String[] args) throws IOException {
7 Path tempFile = Files.createTempFile("before-read", ".txt");
8 Files.writeString(tempFile, "Order 101\nOrder 102\nOrder 103");
9
10 StringBuilder content = new StringBuilder();
11 try (FileReader reader = new FileReader(tempFile.toFile())) {
12 int ch;
13 while ((ch = reader.read()) != -1) {
14 content.append((char) ch);
15 }
16 }
17
18 System.out.println(content);
19
20 Files.delete(tempFile);
21 }
22}Output:
Order 101
Order 102
Order 103
Files.readAllLines() does the same job in one call, returning each line already split out as a List<String>.
1// File: AfterReadingFiles.java
2import java.io.IOException;
3import java.nio.file.*;
4import java.util.List;
5
6public class AfterReadingFiles {
7 public static void main(String[] args) throws IOException {
8 Path tempFile = Files.createTempFile("after-read", ".txt");
9 Files.writeString(tempFile, "Order 101\nOrder 102\nOrder 103");
10
11 List<String> lines = Files.readAllLines(tempFile);
12 lines.forEach(System.out::println);
13
14 Files.delete(tempFile);
15 }
16}Output:
Order 101
Order 102
Order 103
For most everyday text files, a one-line Files call replaces several lines of manual character accumulation entirely.
How It Works Internally
One sentence before the diagram: every reading approach ultimately pulls bytes off the same disk, but they differ sharply in how much of the file sits in memory at once and when.
Files.readString() / readAllLines() --> entire file loaded into memory,
then handed back as one object
Files.lines() --> one line pulled from disk at a
time, only as the stream is
consumed, file handle stays open
Scanner --> reads in chunks internally, then
re-parses each chunk into tokens
via regex matching
FileInputStream --> raw bytes only, no character
decoding or line-boundary logic
at all
Files.lines() returns a lazily-evaluated Stream<String> backed by an open file handle, reading one line at a time instead of loading the whole file into memory — it must be used inside a try-with-resources block so that file handle is closed once the stream is done with.
1// File: FilesLinesExample.java
2import java.io.IOException;
3import java.nio.file.*;
4import java.util.stream.*;
5
6public class FilesLinesExample {
7 public static void main(String[] args) throws IOException {
8 Path tempFile = Files.createTempFile("lines-demo", ".txt");
9 Files.writeString(tempFile, "apple\nbanana\ncherry\navocado");
10
11 try (Stream<String> lines = Files.lines(tempFile)) {
12 long count = lines.filter(line -> line.startsWith("a")).count();
13 System.out.println("Lines starting with 'a': " + count);
14 }
15
16 Files.delete(tempFile);
17 }
18}Output:
Lines starting with 'a': 2
Files.lines() holds a file handle open for as long as the stream is being consumed. Skipping the try-with-resources block, or trying to reuse the stream after it closes, is one of the most common bugs beginners hit with this method.
Scanner reads whitespace-delimited tokens rather than raw lines by default, which makes it convenient for parsing a file of numbers or simple space-separated values without manually splitting each line.
1// File: ScannerFileExample.java
2import java.io.*;
3import java.nio.file.*;
4import java.util.Scanner;
5
6public class ScannerFileExample {
7 public static void main(String[] args) throws IOException {
8 Path tempFile = Files.createTempFile("scanner-demo", ".txt");
9 Files.writeString(tempFile, "42 17 8 99");
10
11 int total = 0;
12 try (Scanner scanner = new Scanner(tempFile.toFile())) {
13 while (scanner.hasNextInt()) {
14 total += scanner.nextInt();
15 }
16 }
17
18 System.out.println("Total: " + total);
19
20 Files.delete(tempFile);
21 }
22}Output:
Total: 166
Text-oriented methods assume an encoding, which makes them the wrong tool for binary data — FileInputStream reads raw bytes with no interpretation at all, the only correct choice among these for a file that is not text, such as an image or a serialized data format.
1// File: FileInputStreamExample.java
2import java.io.*;
3import java.nio.file.*;
4
5public class FileInputStreamExample {
6 public static void main(String[] args) throws IOException {
7 Path tempFile = Files.createTempFile("bytes-demo", ".bin");
8 Files.write(tempFile, new byte[] {10, 20, 30, 40});
9
10 try (FileInputStream in = new FileInputStream(tempFile.toFile())) {
11 int total = 0;
12 int b;
13 while ((b = in.read()) != -1) {
14 total += b;
15 }
16 System.out.println("Byte sum: " + total);
17 }
18
19 Files.delete(tempFile);
20 }
21}Output:
Byte sum: 100
Real-World Example
A daily sales report needs to be parsed from a CSV file and totaled — combining Files.lines() with a stream pipeline to skip the header row, parse each remaining row into a record, and sum the results. Files.lines() returning a Stream<String> is what makes this possible: every operation in the Streams API, from filter to collect, applies to a stream of file lines exactly as it would to any other stream, and a record makes a natural target type for a parsed line.
1// File: SalesRecord.java
2
3public record SalesRecord(String date, double amount) {}1// File: SalesFileReader.java
2import java.io.IOException;
3import java.nio.file.*;
4import java.util.*;
5import java.util.stream.*;
6
7public class SalesFileReader {
8
9 public List<SalesRecord> readSales(Path csvFile) throws IOException {
10 try (Stream<String> lines = Files.lines(csvFile)) {
11 return lines
12 .skip(1)
13 .map(line -> line.split(","))
14 .map(parts -> new SalesRecord(parts[0], Double.parseDouble(parts[1])))
15 .collect(Collectors.toList());
16 }
17 }
18}1// File: SalesFileReaderDemo.java
2import java.io.IOException;
3import java.nio.file.*;
4import java.util.List;
5
6public class SalesFileReaderDemo {
7 public static void main(String[] args) throws IOException {
8 Path csvFile = Files.createTempFile("sales", ".csv");
9 Files.writeString(csvFile, """
10 date,amount
11 2026-08-01,12500.0
12 2026-08-02,9800.0
13 2026-08-03,15300.0
14 """);
15
16 SalesFileReader reader = new SalesFileReader();
17 List<SalesRecord> sales = reader.readSales(csvFile);
18
19 double total = sales.stream().mapToDouble(SalesRecord::amount).sum();
20
21 sales.forEach(sale -> System.out.println(sale.date() + ": " + sale.amount()));
22 System.out.println("Total: " + total);
23
24 Files.delete(csvFile);
25 }
26}Output:
2026-08-01: 12500.0
2026-08-02: 9800.0
2026-08-03: 15300.0
Total: 37600.0
A mistake that appears often in fresher pull requests is forgetting that Files.lines() returns a lazily-evaluated stream backed by an open file handle, and either skipping the try-with-resources entirely or trying to reuse the stream after its underlying reader has already closed. Keeping the entire pipeline — filtering, mapping, and collecting — inside the same try-with-resources block, exactly as readSales does here, ties the file handle's lifetime correctly to when it is actually needed.
Best Practices
Use Files.readString() or Files.readAllLines() for small files where holding the entire content in memory is not a concern — they are the simplest option by far.
Reach for Files.lines() inside try-with-resources specifically when a file might be large enough that loading it entirely into memory would be wasteful or risky.
Prefer Scanner only when its token-based parsing is genuinely useful, such as reading whitespace-separated numbers — for straightforward line-by-line text reading, BufferedReader or Files.lines() is both simpler and faster.
Use a byte-oriented stream like FileInputStream for anything that is not text, rather than forcing binary data through a character-based reader that assumes an encoding.
Common Mistakes
Assuming a file exists before reading it, rather than checking first as this section's File & Path Basics article covers, leads directly to an unhandled exception.
1// File: ReadMissingFileMistake.java
2import java.io.IOException;
3import java.nio.file.*;
4
5public class ReadMissingFileMistake {
6 public static void main(String[] args) {
7 try {
8 Files.readAllLines(Path.of("does-not-exist-98765.txt"));
9 } catch (IOException e) {
10 System.out.println("Caught: " + e.getClass().getSimpleName());
11 }
12 }
13}Output:
Caught: NoSuchFileException
Assuming Scanner.next() reads an entire line, the way nextLine() does, is a second, genuinely common mistake — by default, Scanner splits on whitespace, so next() returns only the first token.
1// File: ScannerDelimiterMistake.java
2import java.io.*;
3import java.nio.file.*;
4import java.util.Scanner;
5
6public class ScannerDelimiterMistake {
7 public static void main(String[] args) throws IOException {
8 Path tempFile = Files.createTempFile("scanner-mistake", ".txt");
9 Files.writeString(tempFile, "New York\nLos Angeles");
10
11 try (Scanner scanner = new Scanner(tempFile.toFile())) {
12 System.out.println("First token: " + scanner.next());
13 }
14
15 Files.delete(tempFile);
16 }
17}Output:
First token: New
Reading "New York\nLos Angeles" with next() returns just "New", not the full first line — nextLine() is the method that reads up to the next line terminator.
Interview Questions
Q1. What are the main ways to read a file's content in Java, and when would you choose each?
Files.readString() or Files.readAllLines() for small files loaded entirely into memory, Files.lines() for large files streamed lazily, Scanner for token-based parsing of numbers or delimited values, and a byte-oriented stream like FileInputStream for binary data. The nuance interviewers listen for is whether you can justify each choice by memory footprint and data type, not just name the methods.
Q2. What is the difference between Files.readAllLines() and Files.lines()?
Files.readAllLines() reads the entire file immediately and returns a fully-populated List<String>. Files.lines() returns a lazily-evaluated Stream<String> backed by an open file handle, reading lines on demand rather than loading everything into memory up front. Product-company interviewers often push further, asking what happens to memory usage as file size grows for each — the answer is that only Files.lines() stays flat.
Q3. Why must Files.lines() be used inside a try-with-resources block?
Because it holds a file handle open for as long as the stream is being consumed — the stream itself is AutoCloseable, and failing to close it leaves the underlying file resource open even after the code no longer needs it. This is a common filter question because it exposes whether a candidate actually understands lazy streams versus assuming all streams behave like in-memory collections.
Q4. What happens when you call Files.readAllLines() on a file that does not exist?
It throws NoSuchFileException, a subtype of IOException, exactly as demonstrated in this article's Common Mistakes section — checking existence first, as covered in this section's File & Path Basics article, avoids relying on catching this exception for normal control flow. The nuance being tested is whether you treat exceptions as control flow or as genuine failure signals.
Q5. What is Scanner's default delimiter when reading tokens from a file, and how does it differ from reading lines?
By default, Scanner splits on whitespace, including spaces and line terminators, so next() returns the next whitespace-delimited token rather than the next full line — nextLine() is the method that reads up to the next line terminator instead. This trips up freshers constantly, which is exactly why it gets asked so often.
Q6. What is the difference between a character stream (Reader) and a byte stream (InputStream) in Java's I/O API?
A Reader, such as FileReader, decodes bytes into characters using a specified or default encoding, making it appropriate for text. An InputStream, such as FileInputStream, reads raw bytes with no interpretation at all, making it the correct choice for binary data like images or serialized formats. Interviewers listen for whether you connect this to encoding correctness, not just memorize the class names.
Q7. Is Files.readString() suitable for reading a very large file?
No. It reads the entire file into memory as a single String, which is fine for small to medium files but risks excessive memory use for a very large one — Files.lines() is the better choice when a file's size is a concern. A strong product-company answer names the actual risk: an OutOfMemoryError under real production file sizes, not just "it's slow."
FAQs
Which method should be used to read a whole file into a single String?
Files.readString(), covered in this series' Java 11 Features article, is the simplest way — it reads the entire file's content and decodes it as UTF-8 in one call.
Does Files.readAllLines() include the newline characters in each returned line?
No. Each returned String has its line terminator stripped, consistent with how BufferedReader.readLine() behaves internally, since Files.readAllLines() is built on top of it.
Can Files.lines() be used with a stream pipeline like filter and map?
Yes, exactly as FilesLinesExample and this article's real-world example both demonstrate — Files.lines() returns an ordinary Stream<String>, so every intermediate and terminal operation in the Streams API applies to it directly.
What encoding does Files.readString() assume?
UTF-8, always — it does not detect or adapt to a file's actual encoding, so reading a file saved in a different encoding can throw an exception rather than silently producing incorrect text.
Is Scanner slower than BufferedReader for reading a file line by line?
Yes, generally. Scanner does more work internally, including regex-based tokenizing, even when only nextLine() is being called repeatedly, making BufferedReader, covered in this section's next article, the faster choice for simple line-by-line reading.
Can FileReader read a binary file correctly?
No. FileReader decodes bytes into characters using a text encoding, which corrupts binary data that was never meant to be interpreted as text — FileInputStream is the correct choice for binary content.
Does closing a try-with-resources Scanner also close the underlying file?
Yes. Scanner.close() closes the underlying source it was constructed from, so wrapping a file-backed Scanner in try-with-resources, as every example in this article does, correctly releases the file handle when the block exits.
Summary
Java's several ways to read a file each trade off differently between simplicity, memory use, and the kind of data being read — Files.readString() and readAllLines() for small text files loaded entirely into memory, Files.lines() for large files streamed lazily inside try-with-resources, Scanner for token-based parsing, and byte-oriented streams for anything that is not text at all.
The habit worth carrying forward from this article's sales report example is keeping a Files.lines() pipeline entirely inside its try-with-resources block, and reaching for Scanner's token-based reading only when that is genuinely what a file's format calls for, rather than out of habit where a plain line-by-line read would be simpler and faster.
What to Read Next
Learn how to write data into a file.