8.4 try-with-resources and Advanced Exception Techniques
Learn try-with-resources, a core Java 7 feature that eliminates the hassle and mistakes that arise when resources must always be closed after use.
1. The Problem with Resource Management
Files, database connections, and network sockets must be closed after use. The old approach always required closing in finally and was easy to get wrong.
// ❌ Old style - verbose and error-prone
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("data.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) { // null check required
try {
reader.close(); // close() itself can throw an exception!
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. try-with-resources (Java 7+)
Declare resources in try ( ... ) parentheses and close() is called automatically when the try block exits — whether it ended normally or via an exception.
Requirement: The resource class must implement
java.lang.AutoCloseable(orjava.io.Closeable).
// ✅ try-with-resources - concise and safe!
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("File read error: " + e.getMessage());
}
// reader.close() is called automatically! No finally block needed.
Managing Multiple Resources
// Declare multiple resources with semicolons (closed in reverse order: out → in)
try (
FileInputStream in = new FileInputStream("source.txt");
FileOutputStream out = new FileOutputStream("dest.txt")
) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("File copy complete!");
} catch (IOException e) {
e.printStackTrace();
}
Custom AutoCloseable Implementation
class DatabaseConnection implements AutoCloseable {
private final String url;
DatabaseConnection(String url) {
this.url = url;
System.out.println("DB connected: " + url);
}
void query(String sql) {
System.out.println("Query executed: " + sql);
}
@Override
public void close() {
System.out.println("DB connection closed: " + url); // called automatically
}
}
// Usage
try (DatabaseConnection conn = new DatabaseConnection("jdbc:mysql://localhost/shop")) {
conn.query("SELECT * FROM users");
} // conn.close() called automatically here
// Output:
// DB connected: jdbc:mysql://localhost/shop
// Query executed: SELECT * FROM users
// DB connection closed: jdbc:mysql://localhost/shop
3. Multi-catch: Combining Multiple Exceptions
In Java 7+, the | operator lets you handle multiple exceptions in a single catch block.
// ❌ Old style: repeated catch blocks with identical handling
try {
// ...
} catch (FileNotFoundException e) {
System.out.println("Error: " + e.getMessage());
e.printStackTrace();
} catch (ParseException e) {
System.out.println("Error: " + e.getMessage());
e.printStackTrace();
}
// ✅ Multi-catch: combine with |
try {
String data = readFile("config.txt");
int value = Integer.parseInt(data);
} catch (IOException | NumberFormatException e) {
// Handle both exceptions together
System.out.println("Data processing error: " + e.getMessage());
e.printStackTrace();
}
Note: In multi-catch,
eisfinaland cannot be reassigned.
4. Exception Chaining
When catching an exception and wrapping it in another, preserve the original cause. The root cause can be traced through the stack trace.
class DataService {
void loadData(String path) throws DataLoadException {
try {
// Attempt actual file reading
new FileReader(path);
} catch (FileNotFoundException e) {
// Wrap the low-level exception in a high-level exception (preserving cause)
throw new DataLoadException("Data file not found: " + path, e);
}
}
}
class DataLoadException extends Exception {
DataLoadException(String message, Throwable cause) {
super(message, cause); // cause: the original exception is passed along
}
}
// Usage
try {
new DataService().loadData("missing.csv");
} catch (DataLoadException e) {
System.out.println("High-level error: " + e.getMessage());
System.out.println("Cause: " + e.getCause().getMessage()); // FileNotFoundException
e.printStackTrace(); // Prints the full exception chain
}
5. Checked vs Unchecked Exception Summary
| Category | Examples | Handling |
|---|---|---|
| Checked Exception | IOException, SQLException | Must use try-catch or declare throws |
| Unchecked Exception | NullPointerException, IllegalArgumentException | Optional (runtime exception) |
| Error | OutOfMemoryError, StackOverflowError | Generally no handling needed |
// Checked Exception: compiler forces handling
void readFile(String path) throws IOException { // throws declaration required
new FileReader(path);
}
// Unchecked Exception: handling is optional (defensive programming recommended)
void divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("Divisor cannot be zero.");
return a / b;
}
Real-world exception handling patterns:
-
Checked → Unchecked conversion: Wrapping a library method's Checked exception in an Unchecked exception at the API boundary means callers are not forced to write try-catch everywhere, keeping code cleaner.
// Wrap Checked as RuntimeException
static String readFileSafely(String path) {
try { return Files.readString(Path.of(path)); }
catch (IOException e) { throw new RuntimeException("File read failed: " + path, e); }
} -
Include context in exception messages: "File not found" is far less useful for debugging than "Profile image for user ID 123 (/uploads/123.jpg) not found."
-
Abstract try-with-resources with a lambda(helper pattern):
@FunctionalInterface
interface ThrowingSupplier<T> {
T get() throws Exception;
}
static <T> T withDb(String url, ThrowingSupplier<T> action) {
try (var conn = new DatabaseConnection(url)) {
return action.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// Usage
String result = withDb("jdbc:...", () -> {
// DB operations
return "result";
});