skip to content

How does Files.lines() differ from Files.readAllLines(), and what must you be careful about when using it?

level: seniorimportance: should knowfreq 48%

answer

  1. readAllLines = eager List, whole file in memory
  2. lines = lazy Stream, line on demand
  3. Always close Files.lines (try-with-resources)
  4. Leaked file descriptor if unclosed
  5. Lazy I/O error = UncheckedIOException mid-stream
  6. Both default UTF-8

basics

~20 s

Files.readAllLines reads the whole file into a List in memory at once. Files.lines returns a lazy Stream that reads line-by-line, so it handles huge files, but you must close the stream (use try-with-resources) because it holds an open file handle.

solid answer

~40 s

readAllLines(path) eagerly loads every line into a List<String> — simple, but it buffers the entire file in memory, so it is unsafe for large files. lines(path) returns a Stream<String> that is lazy: lines are read on demand as the stream is consumed, giving constant-ish memory and the ability to short-circuit (e.g. findFirst, limit). The catch is that the returned Stream is backed by an open file, so it implements AutoCloseable and MUST be closed — always use it inside try-with-resources, otherwise the file descriptor leaks. Also, because reading happens lazily during terminal operation, an I/O error surfaces as an UncheckedIOException mid-stream rather than at the call site. Both default to UTF-8 (the charset overload exists). Prefer lines() for large or streamed processing and readAllLines() only for small files you genuinely need fully in memory.

go deeper

for a junior

Knows readAllLines gives a List and that there is a streaming alternative for big files.

for a middle

Explains eager vs lazy and that lines is better for large files, and knows to close it.

for a senior

Articulates the resource-lifecycle requirement (try-with-resources, descriptor leak), short-circuiting benefits, and the UncheckedIOException timing.

for a principal

Sets guidance on streaming I/O patterns, memory budgeting for large-file processing, and reviews code for unclosed Files streams as a leak class.

## Two ways to read a text file's lines NIO.2's `Files` offers two line readers with very different memory and lifecycle behavior. ### readAllLines — eager, all in memory ```java List<String> lines = Files.readAllLines(path); // UTF-8 by default ``` This reads the **entire** file and returns a `List<String>`. It is convenient and the list is fully materialized, so there is nothing to close and you can iterate it many times. The cost: **all lines live in memory simultaneously**. A multi-gigabyte log file can cause `OutOfMemoryError`. Use it only for small files. ### lines — lazy stream ```java try (Stream<String> stream = Files.lines(path)) { long errors = stream.filter(l -> l.contains("ERROR")).count(); } ``` `Files.lines(path)` returns a `Stream<String>` that reads the file **lazily**: each line is pulled from the underlying reader only as the stream pipeline demands it. Benefits: - **Bounded memory** — you process one line at a time instead of holding the whole file. - **Short-circuiting** — `findFirst()`, `anyMatch()`, `limit(n)` can stop early without reading the rest of the file. - **Composable** — fits naturally into `filter`/`map`/`collect` pipelines. ## The lifecycle gotcha: you MUST close it Unlike most streams over in-memory collections, a stream from `Files.lines` is **backed by an open file resource**. It implements `AutoCloseable`. If you do not close it, you **leak a file descriptor / handle** — and on some platforms you can later fail to delete or re-open the file. The correct, idiomatic usage wraps it in **try-with-resources** (as above), which calls `close()` automatically even if the pipeline throws. > Rule of thumb: any `Stream` returned by a `Files` method (`lines`, `list`, `walk`, `find`, `newDirectoryStream`) must be closed. ## The error-timing gotcha With `readAllLines`, an `IOException` is thrown right at the call. With `lines`, reading is deferred to the **terminal operation**, and any I/O failure during consumption is rethrown as an **`UncheckedIOException`** (an unchecked wrapper) from inside the stream pipeline — not as a checked exception at the `Files.lines(...)` call. Your error handling must account for that. ## Charset Both default to **UTF-8**. Each has an overload taking an explicit `Charset` for other encodings. A malformed byte sequence raises a decoding error during reading. ## Choosing between them | Need | Use | |---|---| | Small file, want a reusable List | `readAllLines` | | Large file / streaming / early exit | `lines` (in try-with-resources) | | Lowest-level control / per-line loop without a Stream | `Files.newBufferedReader(path)` then `readLine()` | ## Summary `readAllLines` = eager, simple, memory-hungry. `lines` = lazy, scalable, but you own its lifecycle (close it) and must expect mid-stream `UncheckedIOException`.

  • Why must the stream returned by Files.lines be closed, unlike a stream from a List?
    It is backed by an open file (it holds a file handle/descriptor). A List-backed stream owns no external resource. Failing to close the Files.lines stream leaks the descriptor, so it must be used in try-with-resources.
  • If an I/O error occurs while consuming a Files.lines stream, what exception do you get and where?
    An UncheckedIOException (an unchecked wrapper around the IOException), thrown from inside the terminal operation as lines are read lazily — not a checked exception at the Files.lines call site.

saying these in an interview costs you the question

  • Using Files.lines without try-with-resources (descriptor leak)
  • Calling readAllLines on a huge file (OOM risk)
  • Expecting a checked IOException from Files.lines during consumption
  • Assuming the stream can be reused after close

context