Definition
- Resource streams:
Files.lines(Path)holds a BufferedReader/file handle.Files.walk(Path)andFiles.list(Path)hold directory streams. - In-memory streams:
Collection.stream(),Arrays.stream(),Stream.of(),Stream.generate()hold no OS resource. - AutoCloseable: Stream implements AutoCloseable, enabling try-with-resources for safe cleanup.
- Leak consequence: unclosed file handles accumulate under load until the OS limit (ulimit -n) is reached.
- onClose: registers a callback that fires when
stream.close()is called. - Rule of thumb: if the source is a file, directory, network connection, or database cursor, use try-with-resources.
Collection.stream() is like reading from a printed list on Rajesh's desk at National Bank of India: he can walk away without worrying. Files.lines() is like opening a filing cabinet drawer (file handle): if Rajesh walks away without closing the drawer, nobody else can open it. After 1,000 requests each leaving a drawer open, the cabinet says 'Too many open files!' try-with-resources is the automatic drawer-closer.
Understanding the Concepts
When must you close a resource stream?
Streams backed by I/O resources hold OS file handles that leak if not closed. The safe pattern is try-with-resources.
Which streams must be closed?
// MUST close (hold OS resources):
Stream<String> lines = Files.lines(Path.of("/bank/transactions.csv")); // file handle
Stream<Path> files = Files.walk(Path.of("/bank/reports"), 3); // directory handle
Stream<Path> entries = Files.list(Path.of("/bank/logs")); // directory handle
// Do NOT need closing (in-memory):
Collection.stream(); // reads from List/Set/Map
Arrays.stream(array); // reads from array
Stream.of(elements); // wraps varargs
Stream.generate(supplier); // no OS resource
IntStream.range(0, 100); // generates numbers
What happens without closing?
package com.bank.core;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Stream;
public class LeakyStreamDemo {
// WRONG: file handle never released!
static List<String> leakyRead(Path path) throws IOException {
return Files.lines(path)
.filter(line -> line.contains("SETTLED"))
.toList(); // terminal completes but handle NOT released!
}
// CORRECT: try-with-resources guarantees closure
static List<String> safeRead(Path path) throws IOException {
try (Stream<String> lines = Files.lines(path)) {
return lines
.filter(line -> line.contains("SETTLED"))
.toList();
} // file handle closed HERE, even if exception occurs
}
}
How does onClose work?
Stream<String> lines = Files.lines(path)
.onClose(() -> System.out.println("File handle released for " + path));
// When stream.close() is called (by try-with-resources), the callback fires.
How does this compare to traditional BufferedReader?
| Approach | Auto-close | Streaming | Line processing |
|----------|-----------|-----------|----------------|
| Files.lines + try-with-resources | Yes | Lazy stream | filter/map/collect |
| BufferedReader in try-with-resources | Yes | Line by line | Manual loop |
| Scanner | Manual close needed | Line by line | Manual loop |
// Traditional approach: BufferedReader
try (BufferedReader br = Files.newBufferedReader(path)) {
String line;
while ((line = br.readLine()) != null) {
if (line.contains("SETTLED")) results.add(line);
}
}
// Modern approach: Files.lines stream
try (Stream<String> lines = Files.lines(path)) {
List<String> results = lines.filter(l -> l.contains("SETTLED")).toList();
}
What exceptions can occur?
// UncheckedIOException wraps IOException in stream pipelines
try (Stream<String> lines = Files.lines(Path.of("/bank/missing.csv"))) {
lines.toList(); // throws UncheckedIOException if file not found
} catch (IOException e) {
System.out.println("File not found: " + e.getMessage());
}
// Too many open files: leaked handles accumulate under load
// java.io.IOException: Too many open files
// Fix: ensure every Files.lines is in try-with-resources
What are common mistakes?
// WRONG: assigning to a variable but forgetting try-with-resources
Stream<String> lines = Files.lines(path);
List<String> result = lines.filter(l -> l.contains("SETTLED")).toList();
// Handle leaked! No close() called.
// WRONG: returning a stream from a method without documenting close requirement
public Stream<String> getTransactions() throws IOException {
return Files.lines(Path.of("/bank/txns.csv")); // caller must close!
}
// CORRECT: close inside the method
public List<String> getTransactions() throws IOException {
try (Stream<String> lines = Files.lines(Path.of("/bank/txns.csv"))) {
return lines.toList();
}
}
What about JDBC and database streams?
// Spring Data JPA: queryForStream must be closed
try (Stream<Account> accounts = repository.streamAllActive()) {
accounts.filter(a -> a.balance().compareTo(threshold) > 0)
.forEach(this::processAccount);
} // JDBC ResultSet and Connection released
JDBC-backed streams hold database connections. Forgetting to close them exhausts the connection pool.
What should a senior engineer know?
- Always check the Javadoc of stream-producing methods. Files.lines explicitly states 'the returned stream encapsulates a Reader' and recommends try-with-resources.
- Under production load with connection pooling and request concurrency, leaked file descriptors are a common cause of outages.
- The
ulimit -ncommand shows the OS file descriptor limit. Default is often 1024 on Linux. - GC may eventually close leaked handles, but there is no guarantee of when. Explicit closing is mandatory.
- Spring Data JPA's
@Querystream methods must also be closed.
Visual Overview
Resource streams: In-memory streams: ┌────────────────────────┐ ┌─────────────────────┐ │ Files.lines(path) │ │ list.stream() │ │ Files.walk(path) │ │ Arrays.stream(arr) │ │ Files.list(path) │ │ Stream.of(...) │ │ JDBC queryForStream() │ │ Stream.generate() │ │ │ │ │ │ HOLD file/dir handle │ │ No OS resource │ │ MUST use try-with-res │ │ No close needed │ └────────────────────────┘ └─────────────────────┘
Key Takeaway
Streams backed by I/O resources must be closed with try-with-resources. Files.lines, Files.walk, and Files.list hold OS file handles that leak under load, eventually causing 'Too many open files' errors. Collection.stream() reads from in-memory data and needs no closing.
The safe pattern is try (Stream which guarantees the handle is released even if an exception occurs. Stream.onClose() registers a cleanup callback.
The rule: if the source is a file, directory, network, or database cursor, wrap in try-with-resources. If the source is an in-memory collection, no closing is needed.
Remember one sentence: Files.lines needs try-with-resources to close the file handle; collection.stream() reads from memory and needs no closing.
---