Files.lines()

Stream a file line by line with flat memory use, close it correctly, and handle the UncheckedIOException it throws mid-stream.

Signature
public static Stream<String> lines(Path path, Charset cs) throws IOException
  • Added in Java Java 8

Parameters

ParameterTypeDescription
$pathPathThe file to read.
$csoptionalCharset= StandardCharsets.UTF_8The charset used to decode the bytes. Leave it out to use lines(Path), which decodes as UTF-8.

Return value

Stream<String>A lazily populated stream of the file's lines, without line terminators. The stream holds an open file and must be closed.

Errors

IOException when the file cannot be opened (NoSuchFileException when it does not exist). Errors while reading, including bad bytes for the charset, are thrown later as UncheckedIOException from the stream operation that triggered the read.

Files.lines(path) returns a Stream<String> that reads the file as the stream is consumed. Only a buffer is in memory at any time, so a 5 GB access log costs the same heap as a 5 KB one. It is the tool for logs, exports and import files whose size you do not control.

Always close it

The stream keeps the file open until it is closed, and a terminal operation such as count() does not close it. Leak enough of them in a long-running service and you hit the process limit on open files, seen as Too many open files. Use try-with-resources every time:

try (Stream<String> lines = Files.lines(log)) {
    long errors = lines.filter(l -> l.contains(" ERROR ")).count();
}

Do not return the stream from a method unless the caller knows it must close it. Returning a collected result is usually simpler.

Laziness works for you

Short-circuiting operations stop reading. findFirst, anyMatch and limit read only as far as they need, so finding the first failed order in a large file can stop near the top.

Errors arrive as UncheckedIOException

Opening the file can throw a checked IOException. But a stream cannot throw checked exceptions, so a problem found while reading, such as a byte sequence that is not valid UTF-8, is wrapped in UncheckedIOException and thrown from the terminal operation. Catch it around the stream and call getCause() to get the original IOException.

Mistakes people make

  • Calling Files.lines(path).count() without a try block: the file handle leaks.
  • Expecting the stream to be reusable. A stream can be consumed once; open a new one for a second pass.
  • Using parallel() on it hoping for speed. Disk reads and per-line work in a typical service rarely benefit, and ordering becomes harder to reason about.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Scan a multi-gigabyte access log for 5xx responses in a support script without raising the heap size.
  • Import a large CSV upload row by row into a database in batches, keeping memory use constant.
  • Check whether a file contains a marker line, such as a completed-export footer, and stop reading as soon as it is found.

Frequently asked questions

Do I need to close the stream returned by Files.lines?

Yes. It holds an open file until close() is called, and terminal operations do not close it. Use try (Stream<String> lines = Files.lines(path)) { ... }.

Why does Files.lines throw UncheckedIOException?

Stream operations cannot throw checked exceptions, so an IOException that happens while reading, often MalformedInputException from a non-UTF-8 file, is wrapped. Call getCause() for the original exception, or pass the correct charset.

Is Files.lines faster than BufferedReader?

Both read through a buffered reader, so speed is similar. Files.lines gives you the Stream API; a BufferedReader loop is easier when the per-line code throws checked exceptions.

More examples

Stopping early with findFirst

findFirst is short-circuiting, so the pipeline stops pulling lines after the first match. The file is read in buffered chunks underneath, but no more lines are decoded and passed through the pipeline than needed.

Example · java
void main() throws IOException {
    Path file = Files.createTempFile("orders", ".log");
    try {
        List<String> rows = IntStream.range(0, 20_000)
                .mapToObj(i -> "order-%05d status=%s".formatted(i, i % 1000 == 42 ? "FAILED" : "OK"))
                .toList();
        Files.write(file, rows);

        AtomicInteger examined = new AtomicInteger();
        try (Stream<String> lines = Files.lines(file)) {
            Optional<String> firstFailure = lines
                    .peek(line -> examined.incrementAndGet())
                    .filter(line -> line.endsWith("FAILED"))
                    .findFirst();
            IO.println("found: " + firstFailure.orElse("none"));
        }
        IO.println("lines examined: " + examined.get() + " of " + rows.size());
    } finally {
        Files.deleteIfExists(file);
    }
}

// Output:
// found: order-00042 status=FAILED
// lines examined: 43 of 20000

Catching UncheckedIOException

The file opens without error; the decoding failure only happens when count() reads it, so it surfaces as UncheckedIOException. Every byte is valid in ISO-8859-1, so the second stream succeeds.

Example · java
void main() throws IOException {
    Path file = Files.createTempFile("import", ".txt");
    try {
        // 0xFF never appears in valid UTF-8.
        Files.write(file, new byte[] {'o', 'k', '\n', (byte) 0xFF, '\n'});

        try (Stream<String> lines = Files.lines(file)) {
            IO.println("lines: " + lines.count());
        } catch (UncheckedIOException e) {
            IO.println("wrapped: " + e.getCause().getClass().getSimpleName());
        }

        try (Stream<String> lines = Files.lines(file, StandardCharsets.ISO_8859_1)) {
            IO.println("Latin-1 lines: " + lines.count());
        }
    } finally {
        Files.deleteIfExists(file);
    }
}

// Output:
// wrapped: MalformedInputException
// Latin-1 lines: 2

Summing a large CSV without loading it

Each line is parsed and dropped as soon as it has been added, so memory stays flat however many rows the export has. skip(1) drops the header row before parsing.

Example · java
void main() throws IOException {
    Path file = Files.createTempFile("payments", ".csv");
    try {
        List<String> rows = new ArrayList<>();
        rows.add("id,amount_pence");
        for (int i = 1; i <= 50_000; i++) {
            rows.add(i + "," + (i % 100));
        }
        Files.write(file, rows);

        try (Stream<String> lines = Files.lines(file)) {
            long total = lines.skip(1) // header
                    .mapToLong(line -> Long.parseLong(line.substring(line.indexOf(',') + 1)))
                    .sum();
            IO.println("total pence: " + total);
        }
    } finally {
        Files.deleteIfExists(file);
    }
}

// Output:
// total pence: 2475000

Discussion

  • Be the first to comment on this lesson.