try-with-resources and cleanup
AutoCloseable, suppressed exceptions, finally's foot-guns, and the resource leak that shows up as a connection pool exhausted at 3am.
A resource is anything you must give back: a file handle, a socket, a database connection, a lock. Forgetting to give it back is a leak, and a leak in a server is not a crash — it is a service that works for three days and then stops accepting connections. try-with-resources is the mechanism that makes giving back automatic, and it has two subtleties that the finally block it replaced could never get right.
The old way and why it was wrong
InputStream in = null;
try {
in = Files.newInputStream(path);
process(in);
} finally {
if (in != null) in.close(); // if close() throws, the original exception is lost
}Three problems. It is verbose. If process throws and close throws, the exception from close replaces the one from process, and you debug the wrong failure. And with two resources, the nesting doubles.
try-with-resources
try (InputStream in = Files.newInputStream(path);
BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) {
return reader.lines().count();
}Anything that implements AutoCloseable can be declared in the parentheses. The compiler generates the close calls — in reverse order of declaration, so reader closes before in — whether the block completes, returns, or throws. Since Java 9 an effectively-final variable declared earlier can be used: try (in) { ... }.
Suppressed exceptions
If the body throws and a close() also throws, the body's exception is the one propagated, and the close() exception is attached to it as suppressed:
try (var r = new Resource()) {
throw new IllegalStateException("body failed");
}
// caught: IllegalStateException: body failed
// Suppressed: java.io.IOException: close failede.getSuppressed() returns them; stack traces print them. The primary failure is preserved, and the secondary one is not lost. This is the behaviour the manual finally could not produce.
finally's remaining uses and its foot-guns
finally still exists for cleanup that is not a resource — resetting a flag, restoring a thread's context class loader, releasing a lock acquired with lock() (which is not AutoCloseable). Two things to never do in it:
returninfinally. It discards any exception thrown in the body, silently. Same forbreakandcontinue.- Throwing from
finally. It replaces the in-flight exception, the same losstry-with-resourceswas designed to prevent.
lock.lock();
try {
mutate();
} finally {
lock.unlock(); // correct: the one thing finally is still for
}Implementing AutoCloseable
Your own classes that own resources should implement it, so callers can use them in try:
public final class BatchWriter implements AutoCloseable {
private final Connection conn;
private boolean closed;
@Override public void close() {
if (closed) return; // idempotent: closing twice is harmless
closed = true;
conn.close();
}
}close() should be idempotent, should release everything even if one release fails (catch, add as suppressed, continue), and should not throw a checked exception unless it genuinely must — AutoCloseable.close() declares throws Exception, but you can narrow it to nothing, which spares every caller a catch.
The leak that shows up at 3am
public List<Order> load(long customerId) {
Connection c = dataSource.getConnection(); // borrowed from the pool
PreparedStatement ps = c.prepareStatement(SQL);
ps.setLong(1, customerId);
ResultSet rs = ps.executeQuery(); // throws on a bad row
...
c.close(); // never reached on the throw
}One bad row means one connection never returned. HikariCP's default pool is ten. Ten bad rows over a day and every request waits thirty seconds for a connection that will never come, then fails with SQLTransientConnectionException: connection is not available. The service is up; every endpoint that touches the database is down. This is the single most common resource leak in Java services, and the fix is the three-resource try you would write on the first day of a Java course.
Streams that are resources
Files.lines(path), Files.list(dir), Files.walk return streams that hold an open handle. Close them: try (Stream<String> lines = Files.lines(path)) { ... }. A stream from a collection does not need this; a stream from the file system does.