skip to content

Why must I/O streams be closed, and what happens if you forget to close one?

level: juniorimportance: must knowfreq 78%

answer

  1. Streams = finite OS file descriptors/handles, not just memory
  2. Forgetting close leaks handles -> 'Too many open files'
  3. close() flushes buffered output first
  4. GC/finalizers do NOT reliably release handles
  5. try-with-resources guarantees close, reverse order

basics

~20 s

Streams hold operating-system resources like file handles or sockets. If you never close them, those resources leak and can run out, and buffered data may never be written. Always close them when you are done.

solid answer

~40 s

An I/O stream wraps a finite OS resource (a file descriptor, socket, pipe). The JVM and the OS limit how many a process can hold open, so leaking them eventually throws 'too many open files' and can lock files on Windows. Closing also flushes buffered output, so a forgotten close can mean data silently never reaches the file. Garbage collection is not a reliable safety net: finalizers are unspecified in timing and largely removed in modern Java, so you cannot rely on the collector to release handles. The correct pattern is try-with-resources, which guarantees close() runs even on exceptions, in reverse order of acquisition. Manual try/finally works too but is error-prone. Closing is idempotent for most JDK streams, so a redundant close is harmless.

code

java · 11 lines
java
// Leak: never closed; on exception the handle stays open
FileInputStream bad = new FileInputStream("data.txt");
bad.read();

// Correct: try-with-resources guarantees close() even on exception
try (FileInputStream in = new FileInputStream("data.txt")) {
    int b = in.read();
} catch (IOException e) {
    // handle
}
// in.close() has already run here

go deeper

for a junior

Knows streams must be closed and that try-with-resources is the way to do it; can explain that unclosed streams 'leak'.

for a middle

Explains file descriptors/handles, the 'too many open files' failure, Windows file locking, and that close() flushes; uses try-with-resources by reflex.

for a senior

Articulates why GC/finalizers are unreliable, suppressed exceptions in try-with-resources, reverse close order, idempotency, and closing only the outer wrapper.

for a principal

Frames resource lifecycle as a systemic concern: descriptor limits as a capacity/ops constraint, the move away from finalizers to Cleaner/AutoCloseable, and codebase-wide conventions/linting to prevent leaks.

## What an I/O stream is In Java, an **I/O stream** is an object that moves a sequence of data, one piece at a time, between your program and some external place: a file on disk, a network socket, the console, or memory. **Byte streams** (`InputStream`/`OutputStream`) move raw bytes; **character streams** (`Reader`/`Writer`) move text (`char`s). 'Stream' here is the classic `java.io` term and is unrelated to `java.util.stream` (the functional collection API). ## Why closing matters: resources are finite When you open, say, a `FileInputStream`, the JVM asks the operating system for a **file descriptor** (on Unix) or **handle** (on Windows) — a small integer the OS uses to track the open file. The OS caps how many a single process may hold (often ~1024 by default on Linux). Each unclosed stream keeps one descriptor reserved. If a long-running server opens files in a loop and never closes them, it eventually hits the limit and throws `java.io.IOException: Too many open files`, even though there is plenty of memory. On Windows there is a second symptom: an open handle **locks the file**, so other code (or the user) cannot delete or rename it until you close. ## The second reason: buffered data Many output streams **buffer** — they accumulate bytes in memory and write them to the OS in larger chunks for speed (e.g. `BufferedOutputStream`, `BufferedWriter`). `close()` **flushes** that buffer first, then releases the resource. So a missing close can mean the last chunk you 'wrote' was never actually persisted — the file ends up truncated or empty with no error. ## Why you can't rely on garbage collection New Java developers assume the garbage collector will clean up. It won't reliably: (1) the collector frees *memory*, not OS handles; (2) `finalize()`, the old hook some streams used, runs at an unspecified time, may never run before exit, and is deprecated/removed in modern Java; (3) the JVM may exit while objects are still reachable. Relying on GC for resource release is a classic bug. ## The correct pattern: try-with-resources Introduced in Java 7, **try-with-resources** declares the stream in parentheses; the compiler generates a guaranteed `close()` in a `finally`, even if the body throws. Any class implementing `AutoCloseable` (which all streams do) works. Multiple resources close in **reverse order** of declaration (last opened, first closed), which is what wrapping streams need. ```java try (var in = new FileInputStream("a.txt")) { // use in } // close() runs here automatically ``` Before Java 7 you wrote try/finally by hand, which is verbose and easy to get wrong (e.g. forgetting that close() itself can throw). ## Idempotency Most JDK streams make `close()` **idempotent** — calling it twice is harmless. So try-with-resources plus a manual close won't break, though it's redundant. ## Wrapped streams Closing the **outermost** wrapper closes the whole chain: `new BufferedWriter(new FileWriter(f))` — closing the `BufferedWriter` flushes it and then closes the underlying `FileWriter`. You only need to close the outer one.

  • Does close() throw, and how does try-with-resources handle that?
    Yes, close() can throw IOException. With try-with-resources, if the body also throws, the body's exception is the primary one and the close() exception is attached as a *suppressed* exception (visible via Throwable.getSuppressed()).
  • If you wrap a FileWriter in a BufferedWriter, which one do you close?
    Close the outermost wrapper (the BufferedWriter). Its close() flushes the buffer and then closes the underlying FileWriter for you; you should not close the inner stream separately.

A stream is like a library book on loan: the library has a limited number of copies (file descriptors). If you never return books, eventually nobody can borrow one. Returning the book (close) is your responsibility, not the librarian's cleanup later (GC).

saying these in an interview costs you the question

  • Claiming the garbage collector will close streams for you
  • Thinking a leaked stream only wastes memory (it wastes OS handles)
  • Closing the inner stream but not the outer wrapper (or vice versa)
  • Believing close() never needs flushing because you didn't buffer anything

context