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

  1. 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
    }
    
  2. 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();
        }
    }
    
  3. Make close() Idempotent

    @Override
    public void close() throws Exception {
        if (!closed) {
            // actual cleanup
            closed = true;
        }
    }
    
  4. 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);
        }
    }
    
  5. Use Suppressed Exceptions Appropriately

    catch (Exception e) {
        logger.error("Operation failed: {}", e.getMessage());
        for (Throwable suppressed : e.getSuppressed()) {
            logger.warn("Cleanup error: {}", suppressed.getMessage());
        }
    }