Read a File Line by Line
Reading a file line by line is one of the most common tasks in Java — whether you’re parsing a CSV, processing logs, or loading configuration data. Java gives you several clean ways to do it, from the classic BufferedReader to the modern Files.lines() stream.
Why “Line by Line”?
Loading an entire file into memory at once works fine for small files, but a 2 GB log file will crash your JVM with an OutOfMemoryError. Reading one line at a time keeps your memory footprint tiny and constant, no matter how large the file is.
Tip: For most real-world tasks, always prefer line-by-line reading over reading the whole file into a
Stringorbyte[].
Method 1: BufferedReader (Classic, Most Versatile)
BufferedReader wraps a FileReader and buffers chunks of characters internally, so disk reads are batched rather than byte-by-byte. Call readLine() in a loop until it returns null.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadWithBufferedReader {
public static void main(String[] args) {
String path = "notes.txt";
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Could not read file: " + e.getMessage());
}
}
}
Output (assuming notes.txt contains two lines):
Hello, world!
This is line two.
A few things to notice:
- The try-with-resources block (
try (...)) automatically closes the reader, even if an exception is thrown — no need for afinallyblock. readLine()strips the line terminator (\n,\r\n, or\r) from each line.- It returns
null(not an empty string) when the end of the file is reached.
See BufferedReader for the full API reference.
Method 2: Scanner
Scanner is beginner-friendly and works well when you also need to parse tokens (numbers, words) within each line.
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class ReadWithScanner {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File("notes.txt"))) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
} catch (FileNotFoundException e) {
System.err.println("File not found: " + e.getMessage());
}
}
}
Note:
Scanneris convenient but slower thanBufferedReaderbecause it uses regular-expression matching internally. For large files, preferBufferedReaderorFiles.lines().
Method 3: Files.lines() — Stream API (Java 8+)
Files.lines() returns a lazy Stream<String> where each element is one line. You can chain it with the full power of the Stream API — filter, map, collect, and more.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class ReadWithFilesLines {
public static void main(String[] args) {
try (Stream<String> lines = Files.lines(Paths.get("notes.txt"))) {
lines.forEach(System.out::println);
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Because Stream<String> implements AutoCloseable, use try-with-resources here too — otherwise the underlying file handle stays open.
Filtering Lines with Streams
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FilterLines {
public static void main(String[] args) throws IOException {
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
List<String> errors = lines
.filter(line -> line.startsWith("ERROR"))
.collect(Collectors.toList());
errors.forEach(System.out::println);
}
}
}
Output (for a log file with mixed levels):
ERROR: Null pointer at line 42
ERROR: Connection timeout after 30s
Method 4: Files.readAllLines() — Simple, Small Files Only
If the file is small and you want all lines as a List<String> in one call, Files.readAllLines() is the simplest option.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class ReadAllLines {
public static void main(String[] args) throws IOException {
List<String> lines = Files.readAllLines(Paths.get("notes.txt"));
for (int i = 0; i < lines.size(); i++) {
System.out.println((i + 1) + ": " + lines.get(i));
}
}
}
Output:
1: Hello, world!
2: This is line two.
Warning:
Files.readAllLines()loads the entire file into memory. Never use it on large files — useFiles.lines()(lazy stream) instead.
Handling Character Encoding
Always specify the encoding explicitly to avoid platform-dependent bugs. UTF-8 is the safe default.
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ReadWithEncoding {
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("utf8file.txt"),
StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
Files.lines() also accepts a Charset as a second argument:
Files.lines(Paths.get("utf8file.txt"), StandardCharsets.UTF_8)
Comparison: Which Method Should You Use?
| Method | Best For | Memory | Java Version |
|---|---|---|---|
BufferedReader | Large files, max control | Low (buffered) | All |
Scanner | Small files, token parsing | Low | All |
Files.lines() | Stream pipelines, large files | Low (lazy) | Java 8+ |
Files.readAllLines() | Small files, quick list | High (all in RAM) | Java 7+ |
Under the Hood
How BufferedReader Works
FileReader reads from a FileInputStream one character at a time (one native system call per character). BufferedReader wraps it and pre-fills an internal char[] buffer (default size 8,192 characters) with each read. Subsequent readLine() calls serve characters from this buffer — so a 10,000-line file may cost only a handful of system calls instead of millions.
How Files.lines() Is Lazy
Files.lines() opens a BufferedReader under the hood and wraps it in a Spliterator. Lines are only read from disk when a terminal operation (like forEach or collect) actually pulls them. This means the full file is never held in memory simultaneously — each line is processed and discarded before the next is read. The stream is backed by the NIO.2 Path API, which uses OS-level file channels for efficient I/O.
Line Terminator Handling
Java normalizes all three common line endings — \n (Unix), \r\n (Windows), \r (old Mac) — into a stripped String. You never have to manually strip \r from the end of lines.
Practical Example: Counting Lines and Words
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
public class WordCounter {
public static void main(String[] args) throws IOException {
AtomicInteger lineCount = new AtomicInteger();
AtomicInteger wordCount = new AtomicInteger();
try (Stream<String> lines = Files.lines(Paths.get("essay.txt"))) {
lines.forEach(line -> {
lineCount.incrementAndGet();
wordCount.addAndGet(line.split("\\s+").length);
});
}
System.out.println("Lines: " + lineCount.get());
System.out.println("Words: " + wordCount.get());
}
}
Output:
Lines: 42
Words: 317
Related Topics
- BufferedReader — full API reference for the most popular line-reading class
- File Class — creating
Fileobjects and inspecting file metadata - Scanner — parsing tokens and user input in addition to files
- Stream API — chaining filter, map, and collect on
Files.lines() - Write to a File — the natural companion to reading — persist data back to disk
- NIO.2: Path & Files — modern file API powering
Files.lines()andFiles.readAllLines()