Recently, I was reading the great book Effective Java, 3rd Edition by Joshua Bloch. An awesome book that I highly recommend to every Java developer. One of the items that caught my attention was Item 9: Prefer try-with-resources to try-finally. Bloch states: “It may be hard to believe, but even good programmers got this wrong most of the time.”

What does that mean, and why is it important? Let’s dive into the details.

The Problem with try-finally

Before Java 7, the common way to manage resources like files, database connections, or network sockets was to use a try-finally block. Consider the following example:

 1public class PreferTryWithResources {
 2
 3    public static void main(String[] args) throws Exception {
 4        try {
 5            readFile();
 6        } finally {
 7            // Simulate resource cleanup that throws an exception
 8            throw new IllegalStateException("Error while cleaning up");
 9        }
10    }
11
12    private static void readFile() throws IOException {
13        for (int i = 0; i < 10; i++) {
14            if (i == 5) {
15                throw new IOException("File has been closed because of an unnecessary condition");
16            }
17
18            System.out.printf("Line %d read%n", i);
19        }
20    }
21
22}

This code attempts to read a file (simulated by a loop for demonstration purposes) and throws a IOException midway through. Assume that the file resource needs to be closed in a finally block to prevent resource leaks. However, if an exception occurs while reading the file, the finally block also throws an exception, which can mask the original exception. This makes debugging difficult, as the original cause of the error is lost.

Have a closer look at the program’s output:

Line 0 read
Line 1 read
Line 2 read
Line 3 read
Line 4 read
Exception in thread "main" java.lang.IllegalStateException: Error while cleaning up
	at test.TryFinallyException.main(TryFinallyException.java:18)

It completely hides the original IOException that occurred while reading the file. This can be especially problematic in complex applications where multiple resources are being managed. If you have a closer look at the example, you might notice that the IOException even suggests a solution to the problem: “File has been closed because of an unnecessary condition”. Just reading the exception message, you can already see that the if statement is unnecessary and should be removed. The try-finally approach can mask such issues, making it harder to identify and fix bugs.

The Solution: Try-With-Resources

Java 7 introduced the try-with-resources statement, which simplifies resource management and automatically handles resource closure. Here’s how you can rewrite the previous example using try-with-resources:

 1public class PreferTryWithResources {
 2
 3    public static void main(String[] args) throws Exception {
 4        try (var reader = new FileReader()) {
 5            reader.readFile();
 6        }
 7    }
 8
 9    private static class FileReader implements AutoCloseable {
10
11        void readFile() throws IOException {
12            for (int i = 0; i < 10; i++) {
13                if (i == 5) {
14                    throw new IOException("File has been closed because of an unnecessary condition");
15                }
16
17                System.out.printf("Line %d read%n", i);
18            }
19        }
20
21        @Override
22        public void close() {
23            // Simulate resource cleanup that throws an exception
24            throw new IllegalStateException("Error while cleaning up");
25        }
26
27    }
28
29}

As you can see, the try-with-resources statement automatically closes the FileReader resource when the try block is exited, whether normally or due to an exception. This approach ensures that the original exception is preserved and any exceptions thrown during resource closure are suppressed, making it easier to diagnose issues.

When you run this program, the output will be:

Line 0 read
Line 1 read
Line 2 read
Line 3 read
Line 4 read
java.io.IOException: File has been closed because of an unnecessary condition
	at test.PreferTryWithResources$FileReader.readFile(PreferTryWithResources.java:27)
	at test.PreferTryWithResources.main(PreferTryWithResources.java:16)
	Suppressed: java.lang.IllegalStateException: Error while cleaning up
		at test.PreferTryWithResources$FileReader.close(PreferTryWithResources.java:36)
		at test.PreferTryWithResources.main(PreferTryWithResources.java:15)

This output clearly shows the original IOException and also indicates that an IllegalStateException occurred during resource cleanup, but it does not mask the original error. Even better, the IOException message still suggests that the if statement is unnecessary and the error that occurred while closing the resource is now just a suppressed exception and is not lost. This makes it much easier to identify and fix the root cause of the problem.

Beyond Files: Using Try-With-Resources for Any Cleanup

The try-with-resources statement is not limited to file handling. Any class that implements the AutoCloseable interface can be used with try-with-resources. For example, consider a custom resource for locking:

 1public class LockExample {
 2
 3    public static void main(String[] args) throws Exception {
 4        try (var lock = new Lock()) {
 5            // Critical section
 6            System.out.println("Lock acquired, performing operations...");
 7        }
 8    }
 9
10    private static class Lock implements AutoCloseable {
11
12        Lock() {
13            // Acquire the lock
14            System.out.println("Lock acquired");
15        }
16
17        @Override
18        public void close() {
19            // Release the lock
20            System.out.println("Lock released");
21        }
22
23    }
24
25}

If an exception occurs within the try block, the close method will still be called to release the lock, ensuring proper resource management. Almost every code that uses try-finally for resource management can be refactored to use try-with-resources, as demonstrated in the example above.

Conclusion

In summary, prefer using try-with-resources for resource management in Java. It simplifies code, improves readability, and ensures that resources are properly closed without masking original exceptions. The try-with-resources statement is a powerful feature that does not only apply to file handling but also to any resource that implements the AutoCloseable interface, such as database connections, network sockets, and for every class you like to implement it.

Under normal circumstances, the try-finally approach should be avoided in favor of try-with-resources to ensure robust and maintainable code. I would even go as far as saying that the try-finally approach should be considered a code smell in modern Java programming as it can often be replaced with the more elegant and safer try-with-resources construct, with some very rare exceptions.