7.2 Try-With-Resources and Resource Management
The try-with-resources statement, introduced in Java 7 and enhanced in Java 9, automatically manages resources that implement AutoCloseable, ensuring they're properly closed even when exceptions occur.
Basic Try-With-Resources
Traditional Finally Block:
// Old way - verbose and error-prone
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
String line = reader.readLine();
// process line
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// What do we do with this exception?
System.err.println("Error closing reader: " + e.getMessage());
}
}
}
Try-With-Resources (Java 7+):
// New way - clean and safe
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line = reader.readLine();
// process line
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
// reader is automatically closed, even if exception occurs
Multiple Resources
Multiple Resources in Try:
// Multiple resources, closed in reverse order of declaration
try (
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line.toUpperCase());
writer.newLine();
}
} catch (IOException e) {
System.err.println("File operation failed: " + e.getMessage());
}
// All resources closed automatically in reverse order:
// writer, reader, fos, fis
Modern Files API:
// Even cleaner with modern APIs
try (Stream<String> lines = Files.lines(Path.of("input.txt"))) {
lines.map(String::toUpperCase)
.forEach(System.out::println);
} catch (IOException e) {
System.err.println("Error processing file: " + e.getMessage());
}
AutoCloseable Interface
Creating Custom Resources:
// Any class can implement AutoCloseable
public class DatabaseConnection implements AutoCloseable {
private final Connection conn;
private boolean closed = false;
public DatabaseConnection(String url) throws SQLException {
this.conn = DriverManager.getConnection(url);
System.out.println("Connection opened");
}
public ResultSet executeQuery(String sql) throws SQLException {
if (closed) {
throw new IllegalStateException("Connection is closed");
}
return conn.createStatement().executeQuery(sql);
}
@Override
public void close() throws SQLException {
if (!closed) {
conn.close();
closed = true;
System.out.println("Connection closed");
}
}
}
// Usage
try (DatabaseConnection db = new DatabaseConnection("jdbc:...")) {
ResultSet rs = db.executeQuery("SELECT * FROM users");
// process results
} catch (SQLException e) {
System.err.println("Database error: " + e.getMessage());
}
// close() called automatically
Lock Management:
public class ManagedLock implements AutoCloseable {
private final Lock lock;
public ManagedLock(Lock lock) {
this.lock = lock;
lock.lock();
}
@Override
public void close() {
lock.unlock();
}
}
// Usage
Lock lock = new ReentrantLock();
try (ManagedLock ml = new ManagedLock(lock)) {
// critical section - lock is held
modifySharedState();
} // lock automatically released
Timer/Stopwatch:
public class Timer implements AutoCloseable {
private final String name;
private final long startTime;
public Timer(String name) {
this.name = name;
this.startTime = System.nanoTime();
System.out.println("[" + name + "] Started");
}
@Override
public void close() {
long duration = System.nanoTime() - startTime;
System.out.printf("[%s] Completed in %.2f ms%n",
name, duration / 1_000_000.0);
}
}
// Usage
try (Timer timer = new Timer("User Query")) {
List<User> users = repository.findAll();
// process users
} // automatically logs execution time
Effectively Final Variables (Java 9+)
Java 7-8 Limitation:
// Java 7-8: Must declare resource in try
InputStream is = new FileInputStream("file.txt");
try (InputStream is2 = is) { // Must create new variable
// use is2
}
Java 9+ Enhancement:
// Java 9+: Can use effectively final variables
InputStream is = new FileInputStream("file.txt");
try (is) { // Use existing variable directly
// use is
} // is closed automatically
Multiple Existing Resources:
BufferedReader reader = Files.newBufferedReader(Path.of("input.txt"));
BufferedWriter writer = Files.newBufferedWriter(Path.of("output.txt"));
try (reader; writer) { // Both closed automatically
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
}
}
Suppressed Exceptions
Understanding Suppressed Exceptions:
public class ResourceWithException implements AutoCloseable {
private final String name;
public ResourceWithException(String name) {
this.name = name;
}
public void doWork() throws Exception {
throw new Exception("Error during work: " + name);
}
@Override
public void close() throws Exception {
throw new Exception("Error closing: " + name);
}
}
// Usage
try (ResourceWithException resource = new ResourceWithException("test")) {
resource.doWork(); // Throws exception
} catch (Exception e) {
System.out.println("Primary: " + e.getMessage());
// Primary: Error during work: test
// Close exception is suppressed
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Suppressed: " + suppressed.getMessage());
// Suppressed: Error closing: test
}
}
Multiple Suppressed Exceptions:
// When multiple resources throw during close
try (
ResourceWithException r1 = new ResourceWithException("first");
ResourceWithException r2 = new ResourceWithException("second");
ResourceWithException r3 = new ResourceWithException("third")
) {
throw new Exception("Error in try block");
} catch (Exception e) {
System.out.println("Primary: " + e.getMessage());
// Primary: Error in try block
// All close exceptions are suppressed (in reverse order)
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Suppressed: " + suppressed.getMessage());
// Suppressed: Error closing: third
// Suppressed: Error closing: second
// Suppressed: Error closing: first
}
}
Real-World Examples
Database Transaction:
public class Transaction implements AutoCloseable {
private final Connection connection;
private boolean committed = false;
public Transaction(Connection connection) throws SQLException {
this.connection = connection;
this.connection.setAutoCommit(false);
}
public void execute(String sql) throws SQLException {
connection.createStatement().execute(sql);
}
public void commit() throws SQLException {
connection.commit();
committed = true;
}
@Override
public void close() throws SQLException {
try {
if (!committed) {
connection.rollback();
System.out.println("Transaction rolled back");
}
} finally {
connection.setAutoCommit(true);
}
}
}
// Usage
try (Transaction tx = new Transaction(connection)) {
tx.execute("INSERT INTO orders VALUES (...)");
tx.execute("UPDATE inventory SET quantity = quantity - 1 WHERE ...");
tx.commit(); // Explicit commit
} catch (SQLException e) {
// If commit fails or exception before commit, rollback happens automatically
System.err.println("Transaction failed: " + e.getMessage());
}
HTTP Client:
public class ManagedHttpClient implements AutoCloseable {
private final HttpClient client;
private final ExecutorService executor;
public ManagedHttpClient() {
this.executor = Executors.newFixedThreadPool(10);
this.client = HttpClient.newBuilder()
.executor(executor)
.build();
}
public HttpResponse<String> get(String url) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
@Override
public void close() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
// Usage
try (ManagedHttpClient client = new ManagedHttpClient()) {
HttpResponse<String> response = client.get("https://api.example.com/data");
System.out.println(response.body());
} // Executor shutdown automatically
Batch Writer:
public class BatchWriter implements AutoCloseable {
private final BufferedWriter writer;
private final List<String> batch = new ArrayList<>();
private final int batchSize;
public BatchWriter(Path file, int batchSize) throws IOException {
this.writer = Files.newBufferedWriter(file);
this.batchSize = batchSize;
}
public void write(String line) throws IOException {
batch.add(line);
if (batch.size() >= batchSize) {
flush();
}
}
public void flush() throws IOException {
for (String line : batch) {
writer.write(line);
writer.newLine();
}
writer.flush();
batch.clear();
}
@Override
public void close() throws IOException {
try {
if (!batch.isEmpty()) {
flush(); // Write remaining items
}
} finally {
writer.close();
}
}
}
// Usage
try (BatchWriter bw = new BatchWriter(Path.of("output.txt"), 100)) {
for (String data : largeDataset) {
bw.write(data);
}
} // Automatically flushes remaining items and closes
Resource Pool:
public class PooledConnection implements AutoCloseable {
private final Connection connection;
private final ConnectionPool pool;
PooledConnection(Connection connection, ConnectionPool pool) {
this.connection = connection;
this.pool = pool;
}
public PreparedStatement prepareStatement(String sql) throws SQLException {
return connection.prepareStatement(sql);
}
@Override
public void close() {
// Don't actually close, return to pool
pool.returnConnection(connection);
}
}
public class ConnectionPool {
private final Queue<Connection> available = new ConcurrentLinkedQueue<>();
private final String url;
public ConnectionPool(String url, int size) throws SQLException {
this.url = url;
for (int i = 0; i < size; i++) {
available.offer(DriverManager.getConnection(url));
}
}
public PooledConnection getConnection() throws SQLException {
Connection conn = available.poll();
if (conn == null) {
conn = DriverManager.getConnection(url);
}
return new PooledConnection(conn, this);
}
void returnConnection(Connection connection) {
available.offer(connection);
}
}
// Usage
ConnectionPool pool = new ConnectionPool("jdbc:...", 10);
try (PooledConnection conn = pool.getConnection()) {
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users");
ResultSet rs = ps.executeQuery();
// process results
} // Connection returned to pool, not closed
Temporary File Manager:
public class TempFile implements AutoCloseable {
private final Path path;
private final boolean deleteOnClose;
public TempFile(String prefix, boolean deleteOnClose) throws IOException {
this.path = Files.createTempFile(prefix, ".tmp");
this.deleteOnClose = deleteOnClose;
System.out.println("Created temp file: " + path);
}
public Path getPath() {
return path;
}
public void write(String content) throws IOException {
Files.writeString(path, content);
}
public String read() throws IOException {
return Files.readString(path);
}
@Override
public void close() throws IOException {
if (deleteOnClose) {
Files.deleteIfExists(path);
System.out.println("Deleted temp file: " + path);
}
}
}
// Usage
try (TempFile temp = new TempFile("processing", true)) {
temp.write("temporary data");
process(temp.getPath());
} // File deleted automatically
Advanced Patterns
Nested Resource Management:
public class DatabaseBackup {
public void backup(String sourceUrl, Path targetFile) throws SQLException, IOException {
try (
DatabaseConnection source = new DatabaseConnection(sourceUrl);
BatchWriter writer = new BatchWriter(targetFile, 1000)
) {
ResultSet rs = source.executeQuery("SELECT * FROM users");
while (rs.next()) {
String line = String.format("%s,%s,%s",
rs.getString("id"),
rs.getString("name"),
rs.getString("email")
);
writer.write(line);
}
}
// Both source and writer closed automatically
}
}
Resource with Configuration:
public class ManagedService implements AutoCloseable {
private final ScheduledExecutorService scheduler;
private final List<ScheduledFuture<?>> tasks = new ArrayList<>();
public ManagedService(int threads) {
this.scheduler = Executors.newScheduledThreadPool(threads);
}
public void scheduleTask(Runnable task, long period, TimeUnit unit) {
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(
task, 0, period, unit
);
tasks.add(future);
}
@Override
public void close() {
// Cancel all tasks
for (ScheduledFuture<?> task : tasks) {
task.cancel(false);
}
// Shutdown scheduler
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
// Usage
try (ManagedService service = new ManagedService(5)) {
service.scheduleTask(() -> System.out.println("Task 1"), 1, TimeUnit.SECONDS);
service.scheduleTask(() -> System.out.println("Task 2"), 2, TimeUnit.SECONDS);
Thread.sleep(10000); // Let tasks run
} // All tasks cancelled and scheduler shutdown automatically
Composite Resource:
public class ApplicationContext implements AutoCloseable {
private final List<AutoCloseable> resources = new ArrayList<>();
public <T extends AutoCloseable> T register(T resource) {
resources.add(resource);
return resource;
}
@Override
public void close() throws Exception {
// Close in reverse order of registration
Exception exception = null;
for (int i = resources.size() - 1; i >= 0; i--) {
try {
resources.get(i).close();
} catch (Exception e) {
if (exception == null) {
exception = e;
} else {
exception.addSuppressed(e);
}
}
}
if (exception != null) {
throw exception;
}
}
}
// Usage
try (ApplicationContext context = new ApplicationContext()) {
DatabaseConnection db = context.register(new DatabaseConnection("..."));
ManagedHttpClient http = context.register(new ManagedHttpClient());
ManagedService service = context.register(new ManagedService(5));
// Use all resources
} // All resources closed in reverse order automatically
Best Practices
Always Use Try-With-Resources for AutoCloseable
// Bad InputStream is = new FileInputStream("file.txt"); try { // use is } finally { is.close(); } // Good try (InputStream is = new FileInputStream("file.txt")) { // use is }Implement AutoCloseable for Cleanup Logic
// Even without actual resources public class UserSession implements AutoCloseable { public UserSession(String userId) { SessionManager.start(userId); } @Override public void close() { SessionManager.end(); } }Make close() Idempotent
@Override public void close() throws Exception { if (!closed) { // actual cleanup closed = true; } }Don't Throw from close() If Possible
@Override public void close() { try { actualClose(); } catch (Exception e) { // Log but don't throw logger.error("Error closing resource", e); } }Use Suppressed Exceptions Appropriately
catch (Exception e) { logger.error("Operation failed: {}", e.getMessage()); for (Throwable suppressed : e.getSuppressed()) { logger.warn("Cleanup error: {}", suppressed.getMessage()); } }