Reading Text Files Line by Line
Fast recall
Quick revision
Core answer
Files.newBufferedReader(path, charset) inside try-with-resources, call readLine() until it returns null, and process each line without loading the whole file.Interview-ready guide
Interview answer
The usual way to read a text file line by line is Files.newBufferedReader(path, charset) in a try-with-resources statement. It returns a BufferedReader, which combines charset decoding, buffering, and the readLine() operation in one practical API. Passing UTF-8 makes the file contract independent of the machine's default charset.
A loop calls readLine() and processes the returned string immediately. The method removes the line-ending characters and returns null only when end of input is reached. An empty string is therefore a real blank line and must not be mistaken for end of file. Since each line can be released after processing, this approach avoids holding the entire file in memory.
For example, a service can read an application log, skip blank lines, and count lines beginning with ERROR. The reader is closed even if parsing a later line throws. IOException still needs to be handled or declared because opening, decoding, reading, and closing can fail.
Files.lines(path, charset) is an alternative that returns a lazy Stream. It is useful for a clear stream pipeline, but the stream wraps an open file and therefore also belongs in try-with-resources. Terminal operations may report a later read failure as UncheckedIOException, which differs from the checked exception used by the explicit loop.
Line-by-line reading bounds total memory but not necessarily per-line memory: a malicious file can contain one enormous line. Applications handling untrusted data may need a size-aware parser rather than readLine(). For normal bounded text files, the buffered-reader loop is explicit, portable, and easy to debug.
Reading Text Files Line by Line
Direct Answer
Open the file with Files.newBufferedReader(path, charset) inside try-with-resources, call readLine() until it returns null, and process each line without loading the whole file.
Deep Dive & Architecture
BufferedReader.readLine() recognizes a line feed, a carriage return, or a carriage return followed by a line feed. It returns the characters before that separator and does not include the separator. Consequently, the caller cannot recover the original choice of line ending from the returned strings alone. If exact byte preservation matters, line-oriented character reading is the wrong abstraction.
The loop is incremental across the file, but a single call still creates a string for one complete line. A 10-gigabyte file made of short records can be processed with small memory, while a single 10-gigabyte line cannot. Record formats with hard limits should enforce them while scanning characters or bytes rather than after readLine() has already allocated the record.
A stream returned by Files.lines does not eagerly read the file. Its close method releases the file, so returning that stream from a method transfers a resource-lifetime problem to the caller. Consuming it entirely inside the method's own try-with-resources block is usually easier to reason about.
Finally, decoding and record parsing are separate failure points. An explicit charset states the encoding agreement; application code still decides whether a line is valid. Keeping those responsibilities separate produces clearer error messages, such as “invalid UTF-8 input” versus “missing field on line 12.”
Code Example
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
class ReadTextLineByLine {
public static void main(String[] args) throws Exception {
Path file = Files.writeString(Files.createTempFile("events-", ".log"),
"INFO start\nERROR disk\n\nERROR network\n", StandardCharsets.UTF_8);
try {
int errors = 0;
try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("ERROR")) errors++;
}
}
if (errors != 2) throw new AssertionError(errors);
System.out.println(errors);
} finally {
Files.deleteIfExists(file);
}
}
}
Check your understanding
Answer these without looking back at the article.
- How does readLine distinguish a blank line from end of file?
- Why must a Stream returned by Files.lines be closed?
- Does line-by-line reading protect against one extremely long line?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.