Back to Stream API Fundamentals
Q6Question 6 of 8

When must you close a resource stream?

Key Concepts
  • Files.lines, Files.walk, and Files.list hold OS file handles and MUST be closed after use.
  • Collection.stream() reads from in-memory data and does NOT need closing.
  • Stream implements AutoCloseable, so try-with-resources is the safe pattern for resource streams.
  • Unclosed file-backed streams leak file descriptors, eventually causing 'Too many open files' errors under load.
  • Stream.onClose() registers a callback that fires when the stream is closed.
  • GC may eventually close leaked handles, but there is no guarantee of when, making explicit closing mandatory.
  • The rule: if the source is a file, directory, network, or DB cursor, wrap in try-with-resources.

Definition

Streams backed by I/O resources (Files.lines, Files.walk) hold OS file handles that must be closed with try-with-resources; collection streams read from memory and need no closing.
  • Resource streams: Files.lines(Path) holds a BufferedReader/file handle. Files.walk(Path) and Files.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?

JAVA
// 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?

JAVA
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?

JAVA
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 |

JAVA
// 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?

JAVA
// 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?

JAVA
// 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?

JAVA
// 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 -n command 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 @Query stream methods must also be closed.

Visual Overview

DIAGRAM
  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 lines = Files.lines(path)) { ... } 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.

---

Share
Use ← → arrow keys to navigate