Skip to content
Java file handling 5 min read

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 String or byte[].


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 a finally block.
  • 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: Scanner is convenient but slower than BufferedReader because it uses regular-expression matching internally. For large files, prefer BufferedReader or Files.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 — use Files.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?

MethodBest ForMemoryJava Version
BufferedReaderLarge files, max controlLow (buffered)All
ScannerSmall files, token parsingLowAll
Files.lines()Stream pipelines, large filesLow (lazy)Java 8+
Files.readAllLines()Small files, quick listHigh (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

  • BufferedReader — full API reference for the most popular line-reading class
  • File Class — creating File objects 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() and Files.readAllLines()
Last updated June 13, 2026
Was this helpful?