Reading a file sounds like it should be simple, and in Java, it mostly is — except there are at least half a dozen ways to do it, and picking the wrong one for the job can mean sluggish performance or a program that runs out of memory on a large file.
Whether you're parsing a config file, processing a data export, or reading raw binary content, Java gives you the tools to do it efficiently. This guide walks through the main approaches, with working code for each.
Why File Handling Matters in Java
Almost every real application touches the filesystem at some point — loading settings, importing data, logging output, or processing user uploads. Java's file-handling classes are split across two main packages:
java.io— the classic I/O library, includingFileReader,BufferedReader, andScanner.java.nio.file— the newer, more modern API built around theFilesandPathsclasses.
Knowing which tool fits which situation is the real skill here — not just knowing that the tool exists.
Quick Comparison: Which Method Should You Use?
| Method | Best For | Memory Usage | Notes |
|---|---|---|---|
FileReader + BufferedReader |
General-purpose text reading | Low (line by line) | The classic, reliable choice |
Files.readAllBytes() |
Small files you need entirely in memory | High (loads whole file) | Simplest syntax, risky for large files |
Files.lines() |
Large text files | Low (streamed) | Great with the Stream API |
Scanner |
Files with mixed data types (numbers, words) | Low to moderate | Slower, but very convenient parsing |
DataInputStream |
Binary files | Low (byte by byte) | Not for plain text |
BufferedInputStream |
Large binary files | Low (buffered) | Faster than raw FileInputStream |
Now let's look at each of these in practice.
1. FileReader + BufferedReader
This is the traditional, tried-and-true approach for reading plain text files, and it's still a perfectly good default choice.
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
public class FileReadExample {
public static void main(String[] args) {
try (FileReader reader = new FileReader("file.txt");
BufferedReader bufferedReader = new BufferedReader(reader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
What's happening here:
FileReaderopens a character stream to the file.BufferedReaderwraps that stream and buffers the input, which cuts down on the number of actual disk reads — a real performance win for anything bigger than a trivial file.readLine()pulls one line at a time, returningnullonce the file is exhausted.- The try-with-resources block automatically closes both readers when you're done, even if an exception is thrown.
This method scales well because it never loads the entire file into memory at once — it just keeps a small buffer and reads line by line.
2. Files.readAllBytes() — Fast and Simple for Small Files
If you're working with a small file and just want its full contents in one shot, Files.readAllBytes() from java.nio.file is about as simple as it gets.
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
public class SmallFileReader {
public static void main(String[] args) {
try {
String content = new String(Files.readAllBytes(Paths.get("file.txt")));
System.out.println(content);
} catch (IOException e) {
e.printStackTrace();
}
}
}
What's happening here:
Paths.get()builds aPathobject pointing to your file.Files.readAllBytes()reads the entire file into a byte array in one call.- Wrapping that in
new String(...)converts it into readable text.
The catch: this loads the whole file into memory at once. For a config file or a small text document, that's a non-issue. For a multi-gigabyte log file, it's a good way to run out of heap space — which is exactly why the next method exists.
3. Files.lines() — The Best Choice for Large Files
When a file is too big to comfortably load all at once, Files.lines() gives you a lazy stream of lines instead — reading only what you actually consume.
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;
public class LineByLineFileReader {
public static void main(String[] args) {
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
lines.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}
What's happening here:
Files.lines()returns aStream<String>, where each element is one line of the file.- Because it's a stream, you can chain it with
filter(),map(),limit(), and other Stream API operations — all without loading the entire file into memory.
Here's a slightly more useful version that filters and counts lines matching a condition:
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;
public class FilteredLineCounter {
public static void main(String[] args) {
try (Stream<String> lines = Files.lines(Paths.get("access.log"))) {
long errorCount = lines
.filter(line -> line.contains("ERROR"))
.count();
System.out.println("Error lines found: " + errorCount);
} catch (IOException e) {
e.printStackTrace();
}
}
}
This pattern — stream, filter, process — is one of the most common ways large text and log files get handled in real Java applications.
4. Scanner — Great for Mixed or Structured Data
Scanner isn't just for reading console input; it also works directly on files, and it shines when a file contains a mix of data types you need to parse out — numbers, words, booleans, and so on.
import java.io.File;
import java.util.Scanner;
import java.io.FileNotFoundException;
public class ScannerFileReader {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File("file.txt"))) {
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
Where Scanner really earns its place is parsing structured values directly, without manual string splitting:
import java.io.File;
import java.util.Scanner;
import java.io.FileNotFoundException;
public class ScannerParsingExample {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File("scores.txt"))) {
while (scanner.hasNext()) {
if (scanner.hasNextInt()) {
int score = scanner.nextInt();
System.out.println("Score: " + score);
} else {
String word = scanner.next();
System.out.println("Name: " + word);
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
Scanner is convenient, but it's also the slowest option on this list — for large files or performance-sensitive code, BufferedReader or Files.lines() is usually the better call.
5. DataInputStream — Reading Binary Data
Not every file is plain text. When you're reading binary formats — images, serialized objects, custom file formats — DataInputStream lets you read primitive data types directly from the byte stream.
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class DataInputStreamExample {
public static void main(String[] args) {
try (DataInputStream dis = new DataInputStream(new FileInputStream("file.txt"))) {
while (dis.available() > 0) {
System.out.print((char) dis.readByte());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
What's happening here:
FileInputStreamopens a raw byte stream to the file.DataInputStreamwraps it, adding methods likereadByte(),readInt(), andreadDouble()for pulling out specific primitive types.available()gives a rough estimate of how many bytes remain unread (not a strict guarantee, but useful as a loop condition here).
6. BufferedInputStream — Faster Binary Reads for Large Files
For large binary files, wrapping your stream in a BufferedInputStream reduces the number of actual disk I/O calls, the same way BufferedReader does for text:
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class BufferedBinaryReader {
public static void main(String[] args) {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("data.bin"))) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
// Process the chunk of bytes read
System.out.println("Read " + bytesRead + " bytes");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Reading in fixed-size chunks like this (1 KB at a time in the example above) keeps memory usage predictable no matter how large the file is — a common pattern when processing large binary files or streaming data over a network.
Handling Exceptions Properly
Every example above uses try-with-resources, which is worth calling out explicitly since it's the modern standard for file handling in Java. Any resource that implements AutoCloseable — which includes all the readers and streams shown here — gets closed automatically once the block finishes, whether it exits normally or via an exception.
Compare the modern approach to the old manual pattern:
// Old style — verbose and easy to get wrong
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
// read the file
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Modern style — try-with-resources handles closing automatically
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
// read the file
} catch (IOException e) {
e.printStackTrace();
}
There's really no reason to use the old pattern in new code — try-with-resources is shorter, safer, and eliminates a whole class of resource-leak bugs.
Choosing the Right Method: A Quick Decision Guide
- Reading a small text file? →
Files.readAllBytes() - Reading a large text file line by line? →
Files.lines()orBufferedReader - Parsing mixed data types (numbers and words together)? →
Scanner - Reading binary data? →
DataInputStreamorBufferedInputStream - Filtering, transforming, or aggregating file contents? →
Files.lines()combined with the Stream API
Final Thoughts
Java doesn't force you into a single way of reading files — it gives you a toolbox, and the right tool depends on the size of the file, whether it's text or binary, and what you actually plan to do with the contents. For most everyday text-processing tasks, BufferedReader or Files.lines() will cover you well. For binary data, lean on DataInputStream or a buffered stream. And whichever method you pick, try-with-resources should be the default — it's one less thing to worry about getting wrong.
Once reading files feels comfortable, the natural next step is writing to them — but that's a topic for another guide.