W3docs

How to Read a File Line by Line in Java

Read a Java file line by line with BufferedReader, Files.lines, Files.readAllLines, and Scanner.

How to Read a File Line by Line in Java

Reading a text file one line at a time is one of the most common file tasks in Java. The JDK gives you several ways to do it; the right choice depends on the file's size and whether you want a plain loop or a stream pipeline. This chapter shows the four idiomatic approaches and when to reach for each.

BufferedReader: the streaming workhorse

BufferedReader.readLine() reads a single line per call and returns null at end of file, so it pairs naturally with a while loop. Wrap it in try-with-resources so the underlying reader closes itself:

Path file = Path.of("notes.txt");
try (BufferedReader br = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
  String line;
  while ((line = br.readLine()) != null) {
    System.out.println(line);
  }
}

This streams the file: only one line is held in memory at a time, so it handles a multi-gigabyte log without trouble. Files.newBufferedReader defaults to UTF-8, but passing the charset explicitly documents intent and avoids platform-dependent decoding. Note that readLine() strips the line terminator (\n, \r, or \r\n), so you never see it in the returned string.

Files.lines: the same job as a Stream

When you want a functional pipeline — filter, map, count — Files.lines gives you a lazy Stream<String> over the file's lines:

try (Stream<String> lines = Files.lines(Path.of("notes.txt"), StandardCharsets.UTF_8)) {
  lines.filter(s -> !s.isBlank())
       .map(String::trim)
       .forEach(System.out::println);
}

Like BufferedReader, it reads lazily and never loads the whole file. The catch is that the stream holds an open file handle, so it must be closed — always use it inside try-with-resources, never as a bare expression. Forgetting this leaks descriptors.

Files.readAllLines: small files, all at once

If the file is small and you want every line in a List<String> up front, Files.readAllLines is the most direct option:

List<String> all = Files.readAllLines(Path.of("notes.txt"), StandardCharsets.UTF_8);
for (String line : all) {
  System.out.println(line);
}

It is eager: the entire file is decoded into memory before you touch the first line. That is convenient for config files and fixtures but a poor fit for large files — prefer the streaming approaches there. Scanner with nextLine() and hasNextLine() is a fourth option, handy when you also need to parse tokens, but it is slower and easy to misuse, so the three above cover most cases.

ApproachMemoryReturnsBest for
BufferedReader.readLine()One linePlain loopLarge files, manual control
Files.lines()One line (lazy)Stream<String>Pipelines on large files
Files.readAllLines()Whole fileList<String>Small files, random access
Scanner.nextLine()One linePlain loopMixed line + token parsing

Worked example: all three side by side

This program writes a tiny temp file (so it is self-contained), then reads it three ways — a BufferedReader loop, a Files.lines stream, and an eager Files.readAllLines:

java— editable, runs on the server

What to take from the run:

  • The BufferedReader loop numbers four lines, and line 3: [] shows that a blank line in the file is returned as an empty string, not skipped — readLine() reports every line, including empty ones.
  • readLine() printed each line without any trailing \n, confirming it strips the line terminator; the only brackets around the text are the literal [ and ] the code added.
  • Files.lines counted non-blank lines: 3 because the filter(s -> !s.isBlank()) dropped the empty line — the stream pipeline operates lazily over the same four lines the loop saw.
  • Files.readAllLines reported total lines: 4 and a first line : alpha, proving it loaded the whole file eagerly into a List<String> you can index with get(0).
  • Each reader sat inside try-with-resources (or returned a managed List), so the file handle and temp file were released cleanly before done printed — no leaked descriptors.

Practice

Practice

Why must a stream returned by Files.lines() be used inside a try-with-resources block?