How do you create a Stream over the lines of a file with Files.lines, and what resource-management concern does it introduce?
answer
- Files.lines → lazy Stream<String>, one line each
- Backed by an open file handle (descriptor)
- Stream is AutoCloseable → try-with-resources
- Terminal op does NOT release the handle
- String.chars()/codePoints() = in-memory, no close
basics
~10 sFiles.lines(path) returns a Stream<String> of the file's lines, read lazily. It holds an open file handle, so you must close it — wrap it in try-with-resources, since Stream is AutoCloseable.
solid answer
~40 sFiles.lines(path) (optionally with a Charset) gives a lazily-populated Stream<String>, one element per line, without loading the whole file into memory — great for large files. The catch: unlike a collection-backed stream, this stream is backed by an open file handle (an I/O resource). Stream implements AutoCloseable, so you must close it to release that handle; the idiom is try-with-resources, because the stream's close runs the underlying reader's close. If you don't, you leak file descriptors. Files.lines also declares IOException, so callers must handle it. A related lazy source is BufferedReader.lines(). For in-memory char sources there's String.chars() / String.codePoints(), which return an IntStream of character values (no resource to close). So the rule of thumb: streams over I/O resources (files, readers) need try-with-resources; streams over in-memory data do not.
code
java · 16 linesimport java.nio.file.*;
import java.util.stream.*;
// Lazy, memory-safe, and resource-safe via try-with-resources:
try (Stream<String> lines = Files.lines(Path.of("data.txt"))) {
long nonBlank = lines.map(String::strip)
.filter(s -> !s.isEmpty())
.count();
System.out.println(nonBlank);
} // file handle closed here, even on exception
// (Files.lines throws IOException — declared or caught by the caller)
// In-memory char source: an IntStream, nothing to close:
long vowels = "functional".chars()
.filter(c -> "aeiou".indexOf(c) >= 0)
.count();go deeper
Knows Files.lines reads a file into a Stream<String> of lines and that you should close it.
Uses try-with-resources because the stream is AutoCloseable, and contrasts lazy Files.lines with eager readAllLines.
Explains the file-descriptor leak, that terminals don't close, the IOException-in-lambda friction, and that Files.walk/list share the rule while in-memory sources don't.
Sets codebase conventions for resource-safe streaming I/O (always wrap, helper to tame checked exceptions in lambdas) and reasons about descriptor exhaustion at scale and Spliterator-backed custom I/O sources.
## Reading a file as a stream of lines ```java try (Stream<String> lines = Files.lines(Path.of("data.txt"))) { long blanks = lines.filter(String::isBlank).count(); } ``` `java.nio.file.Files.lines(Path)` (with an optional `Charset`, defaulting to UTF-8) returns a `Stream<String>` where **each element is one line** of the file. The key property is **laziness**: lines are read from disk *as the pipeline pulls them*, not all at once. So you can process a multi-gigabyte file with constant memory — count lines, filter, map — without an `OutOfMemoryError`, something `Files.readAllLines` (which loads everything into a `List`) cannot do. ## The hidden resource Most streams (from a `List`, an array, `IntStream.range`) own **no external resource** — there's nothing to release. `Files.lines` is different: under the hood it opens the file and holds a **file handle / file descriptor**, an OS-level resource that is *finite*. The stream is therefore tied to that open file. `java.util.stream.Stream` implements **`AutoCloseable`**, and `Files.lines` documents that the returned stream's `close()` method **closes the underlying file**. If you never close it, the descriptor leaks; do that in a loop and you'll eventually hit `Too many open files` and the program fails. ## try-with-resources is mandatory here Because the stream is `AutoCloseable`, the correct idiom is **try-with-resources**, which calls `close()` automatically when the block exits (normally or via exception): ```java try (Stream<String> lines = Files.lines(path)) { return lines.map(String::trim).toList(); } // file handle released here, even on exception ``` Note a subtlety: a *terminal* operation does not necessarily close the stream's resource — closing is the job of `close()` / try-with-resources, not of `count()` or `collect()`. So always wrap I/O-backed streams; don't rely on the terminal to release the handle. ## Checked exception `Files.lines` declares **`throws IOException`** (the file may be missing or unreadable). Inside lambdas this matters: you can call it in the `try`-with-resources header, but if you try to call it *inside* a stream lambda (e.g. `flatMap(p -> Files.lines(p))`), you must handle the checked exception there — a common friction point that often pushes people to a helper method that wraps the IOException. ## Related lazy sources - **`BufferedReader.lines()`** — same idea on a reader; the reader is the resource you must close. - **`Files.list(dir)`**, **`Files.walk(dir)`**, **`Files.find(...)`** — also return `AutoCloseable` streams backed by directory handles; same try-with-resources rule. ## In-memory char sources: no resource For strings there's no file, so no closing: ```java long vowels = "hello".chars() // IntStream of char values .filter(c -> "aeiou".indexOf(c) >= 0) .count(); ``` `String.chars()` returns an **`IntStream`** of the UTF-16 code units (as `int`s); `String.codePoints()` returns an `IntStream` of Unicode code points (handling surrogate pairs). Because the data is already in memory, these own no resource and need **no** try-with-resources. ## The rule of thumb **Stream over an I/O resource (file, reader, directory) → it is AutoCloseable → wrap in try-with-resources.** Stream over in-memory data (collection, array, range, String.chars) → nothing to close. Mixing these up either leaks descriptors (forgot to close an I/O stream) or adds pointless ceremony (wrapping an in-memory stream).
- Does calling .count() on a Files.lines stream close the file?No. The terminal operation consumes the elements but does not release the file handle; you must close the stream (via try-with-resources or an explicit close()) to free the descriptor.
- When is Files.lines preferable to Files.readAllLines?For large files: Files.lines is lazy and streams line-by-line with near-constant memory, whereas readAllLines loads the entire file into a List and can exhaust the heap.
- Why don't String.chars() or IntStream.range() need try-with-resources?They are backed by in-memory data, not an OS resource, so there is no file handle or descriptor to release; they are not the AutoCloseable I/O streams that Files.lines is.
saying these in an interview costs you the question
- Using Files.lines without try-with-resources (leaks file descriptors).
- Assuming a terminal operation like count() closes the underlying file.
- Confusing Files.lines (lazy, closeable) with Files.readAllLines (eager List, no resource).
- Forgetting Files.lines/Files.walk throw IOException and ignoring it.
- Thinking String.chars() needs closing (it doesn't — it's in-memory).