Overview
Java has enhanced try-with-resources statements with pattern matching capabilities, allowing more concise and readable resource management code. This feature was introduced as part of Java's ongoing pattern matching improvements.
Basic Syntax
Traditional try-with-resources
// Before Java 21 - traditional approach
try (InputStream input = new FileInputStream("file.txt");
OutputStream output = new FileOutputStream("output.txt")) {
// Use resources
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
// Handle exception
}
Pattern Matching in try-with-resources (Java 21+)
// Java 21+ - pattern matching with resources
try (var input = new FileInputStream("file.txt");
var output = new FileOutputStream("output.txt")) {
// Resources are automatically managed
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
// Handle exception
}
Pattern Matching with Type Patterns
Basic Type Pattern Matching
import java.io.*;
import java.nio.file.*;
import java.util.*;
public class PatternMatchingResources {
// Pattern matching with custom resources
static class DatabaseConnection implements AutoCloseable {
private final String url;
public DatabaseConnection(String url) {
this.url = url;
System.out.println("Connected to: " + url);
}
public void executeQuery(String query) {
System.out.println("Executing: " + query);
}
@Override
public void close() {
System.out.println("Closing connection to: " + url);
}
}
static class NetworkConnection implements AutoCloseable {
private final String host;
public NetworkConnection(String host) {
this.host = host;
System.out.println("Connected to host: " + host);
}
public void sendData(String data) {
System.out.println("Sending data to " + host + ": " + data);
}
@Override
public void close() {
System.out.println("Closing connection to: " + host);
}
}
public static void basicPatternMatching() {
// Using pattern matching with multiple resources
try (var db = new DatabaseConnection("jdbc:mysql://localhost:3306/mydb");
var network = new NetworkConnection("api.example.com")) {
db.executeQuery("SELECT * FROM users");
network.sendData("Hello, World!");
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Advanced Pattern Matching Features
1. Conditional Resource Initialization
public class ConditionalResources {
public static void processFile(String filename, boolean useCompression)
throws IOException {
// Conditional resource initialization with pattern matching
try (var input = useCompression ?
new GZIPInputStream(new FileInputStream(filename + ".gz")) :
new FileInputStream(filename);
var reader = new BufferedReader(new InputStreamReader(input))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
public static void multipleResourceTypes() {
// Mixing different resource types with pattern matching
try (var scanner = new Scanner(System.in);
var writer = new PrintWriter("output.txt");
var connection = new DatabaseConnection("jdbc:sqlite:test.db")) {
System.out.print("Enter data: ");
String input = scanner.nextLine();
writer.println("User input: " + input);
connection.executeQuery("INSERT INTO logs (data) VALUES ('" + input + "')");
} catch (Exception e) {
System.err.println("Processing failed: " + e.getMessage());
}
}
}
2. Pattern Matching with Exception Handling
import java.sql.*;
import java.util.logging.*;
public class ExceptionHandlingPatterns {
private static final Logger logger = Logger.getLogger(ExceptionHandlingPatterns.class.getName());
static class ConfigurableConnection implements AutoCloseable {
private final boolean shouldFailOnClose;
public ConfigurableConnection(boolean shouldFailOnClose) {
this.shouldFailOnClose = shouldFailOnClose;
}
@Override
public void close() throws Exception {
if (shouldFailOnClose) {
throw new IllegalStateException("Simulated close failure");
}
System.out.println("Connection closed successfully");
}
public void doWork() {
System.out.println("Doing important work...");
}
}
public static void suppressedExceptionsExample() {
// Demonstrating suppressed exceptions with pattern matching
try (var conn1 = new ConfigurableConnection(false);
var conn2 = new ConfigurableConnection(true)) {
conn1.doWork();
conn2.doWork();
} catch (Exception e) {
System.out.println("Primary exception: " + e.getMessage());
// Access suppressed exceptions
Throwable[] suppressed = e.getSuppressed();
for (Throwable suppressedEx : suppressed) {
System.out.println("Suppressed exception: " + suppressedEx.getMessage());
}
}
}
public static void databaseOperationWithRecovery() {
// Complex resource management with recovery logic
try (var connection = DriverManager.getConnection("jdbc:sqlite:sample.db");
var statement = connection.createStatement()) {
// Execute database operations
boolean hasResults = statement.execute("SELECT * FROM users");
if (hasResults) {
try (var resultSet = statement.getResultSet()) {
while (resultSet.next()) {
System.out.println("User: " + resultSet.getString("username"));
}
}
}
} catch (SQLException e) {
logger.severe("Database operation failed: " + e.getMessage());
// Fallback to file-based storage
try (var writer = new PrintWriter("fallback.txt")) {
writer.println("Database unavailable, using fallback storage");
} catch (IOException ioException) {
logger.severe("Fallback also failed: " + ioException.getMessage());
}
}
}
}
Real-World Examples
Example 1: File Processing Pipeline
import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;
import java.util.zip.*;
public class FileProcessingPipeline {
public static void processCompressedFiles(List<Path> filePaths) throws IOException {
for (Path filePath : filePaths) {
// Pattern matching with nested resources
try (var fileInput = Files.newInputStream(filePath);
var gzipInput = new GZIPInputStream(fileInput);
var reader = new BufferedReader(
new InputStreamReader(gzipInput, StandardCharsets.UTF_8))) {
// Process the file content
List<String> lines = reader.lines()
.filter(line -> !line.trim().isEmpty())
.collect(Collectors.toList());
System.out.printf("Processed %s: %d lines%n",
filePath.getFileName(), lines.size());
// Write processed output
Path outputPath = Paths.get("processed_" + filePath.getFileName() + ".txt");
try (var writer = Files.newBufferedWriter(outputPath)) {
for (String line : lines) {
writer.write(line);
writer.newLine();
}
}
}
}
}
public static void multiStageProcessing(Path inputFile) throws IOException {
// Complex pipeline with multiple resource types
try (var input = Files.newInputStream(inputFile);
var decompressor = new GZIPInputStream(input);
var reader = new BufferedReader(new InputStreamReader(decompressor));
var tempFile = Files.createTempFile("processing_", ".tmp");
var tempWriter = Files.newBufferedWriter(tempFile);
var connection = new DatabaseConnection("jdbc:sqlite:data.db")) {
// Stage 1: Read and filter
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("ERROR")) {
tempWriter.write(line);
tempWriter.newLine();
}
}
tempWriter.flush();
// Stage 2: Process filtered data
try (var tempReader = Files.newBufferedReader(tempFile);
var batchWriter = new StringWriter()) {
connection.executeQuery("BEGIN TRANSACTION");
while ((line = tempReader.readLine()) != null) {
// Process each error line
String processed = line.replace("ERROR", "WARNING");
batchWriter.write(processed);
batchWriter.write("\n");
// Batch insert
connection.executeQuery(
"INSERT INTO error_logs (message) VALUES ('" + processed + "')");
}
connection.executeQuery("COMMIT");
// Final output
Path finalOutput = Paths.get("processed_errors.txt");
Files.writeString(finalOutput, batchWriter.toString());
}
} finally {
// Cleanup temporary files
Files.list(Path.of(System.getProperty("java.io.tmpdir")))
.filter(path -> path.getFileName().toString().startsWith("processing_"))
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
System.err.println("Failed to delete temp file: " + path);
}
});
}
}
}
Example 2: HTTP Client with Resource Management
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.file.*;
import java.util.concurrent.*;
public class HttpClientWithResources {
public static class WebResource implements AutoCloseable {
private final HttpClient client;
private final String baseUrl;
public WebResource(String baseUrl) {
this.baseUrl = baseUrl;
this.client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
System.out.println("WebResource initialized for: " + baseUrl);
}
public String fetch(String endpoint) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + endpoint))
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
@Override
public void close() {
System.out.println("Closing WebResource for: " + baseUrl);
// HttpClient doesn't need explicit close in newer Java versions
}
}
public static void concurrentResourceUsage() throws Exception {
// Using multiple web resources with pattern matching
try (var apiClient = new WebResource("https://api.example.com");
var backupClient = new WebResource("https://backup-api.example.com")) {
// Execute concurrent operations
CompletableFuture<String> primaryRequest = CompletableFuture.supplyAsync(() -> {
try {
return apiClient.fetch("/data");
} catch (Exception e) {
return "Primary API failed: " + e.getMessage();
}
});
CompletableFuture<String> backupRequest = CompletableFuture.supplyAsync(() -> {
try {
return backupClient.fetch("/data");
} catch (Exception e) {
return "Backup API failed: " + e.getMessage();
}
});
// Get first successful response
String result = CompletableFuture.anyOf(primaryRequest, backupRequest)
.thenApply(obj -> (String) obj)
.get(5, TimeUnit.SECONDS);
System.out.println("Received: " + result);
} catch (TimeoutException e) {
System.err.println("All requests timed out");
}
}
public static void downloadWithProgress(String url, Path outputPath) throws IOException {
// Complex download with multiple resources and progress tracking
try (var httpClient = HttpClient.newHttpClient();
var input = httpClient.send(
HttpRequest.newBuilder().uri(URI.create(url)).build(),
HttpResponse.BodyHandlers.ofInputStream()).body();
var output = Files.newOutputStream(outputPath)) {
byte[] buffer = new byte[8192];
int bytesRead;
long totalBytes = 0;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
// Progress reporting
if (totalBytes % (1024 * 1024) == 0) { // Every MB
System.out.printf("Downloaded: %.2f MB%n", totalBytes / (1024.0 * 1024.0));
}
}
System.out.println("Download completed: " + totalBytes + " bytes");
}
}
}
Example 3: Transaction Management
import java.sql.*;
import java.util.*;
public class TransactionManagement {
static class TransactionalConnection implements AutoCloseable {
private final Connection connection;
private boolean committed = false;
public TransactionalConnection(String url) throws SQLException {
this.connection = DriverManager.getConnection(url);
this.connection.setAutoCommit(false);
System.out.println("Transaction started");
}
public void executeUpdate(String sql) throws SQLException {
try (Statement stmt = connection.createStatement()) {
stmt.executeUpdate(sql);
}
}
public ResultSet executeQuery(String sql) throws SQLException {
return connection.createStatement().executeQuery(sql);
}
public void commit() throws SQLException {
connection.commit();
committed = true;
System.out.println("Transaction committed");
}
@Override
public void close() throws SQLException {
if (!committed) {
System.out.println("Rolling back transaction");
connection.rollback();
}
connection.close();
System.out.println("Transaction closed");
}
}
public static void transferFunds(String fromAccount, String toAccount,
BigDecimal amount) throws SQLException {
// Using pattern matching for transaction management
try (var tx = new TransactionalConnection("jdbc:sqlite:bank.db")) {
// Check sufficient funds
try (var rs = tx.executeQuery(
"SELECT balance FROM accounts WHERE account_id = '" + fromAccount + "'")) {
if (rs.next()) {
BigDecimal balance = rs.getBigDecimal("balance");
if (balance.compareTo(amount) < 0) {
throw new SQLException("Insufficient funds");
}
} else {
throw new SQLException("Account not found: " + fromAccount);
}
}
// Perform transfer
tx.executeUpdate(
"UPDATE accounts SET balance = balance - " + amount +
" WHERE account_id = '" + fromAccount + "'");
tx.executeUpdate(
"UPDATE accounts SET balance = balance + " + amount +
" WHERE account_id = '" + toAccount + "'");
// Record transaction
tx.executeUpdate(
"INSERT INTO transactions (from_account, to_account, amount) VALUES ('" +
fromAccount + "', '" + toAccount + "', " + amount + ")");
// Commit if all operations succeed
tx.commit();
} // Auto-rollback if exception occurs
}
public static void batchOperations(List<String> operations) throws SQLException {
// Batch processing with transaction
try (var tx = new TransactionalConnection("jdbc:sqlite:batch.db")) {
for (String operation : operations) {
tx.executeUpdate(operation);
}
tx.commit();
} catch (SQLException e) {
System.err.println("Batch operation failed: " + e.getMessage());
throw e;
}
}
}
Best Practices and Patterns
1. Resource Factory Pattern
public class ResourceFactoryPattern {
@FunctionalInterface
public interface ResourceFactory<T extends AutoCloseable> {
T create() throws Exception;
}
public static <T extends AutoCloseable> void withResource(
ResourceFactory<T> factory, ThrowingConsumer<T> consumer) throws Exception {
try (T resource = factory.create()) {
consumer.accept(resource);
}
}
@FunctionalInterface
public interface ThrowingConsumer<T> {
void accept(T t) throws Exception;
}
// Usage examples
public static void factoryPatternUsage() throws Exception {
// File processing
withResource(
() -> new FileInputStream("data.txt"),
input -> {
// Process input stream
byte[] data = input.readAllBytes();
System.out.println("Read " + data.length + " bytes");
}
);
// Database operation
withResource(
() -> DriverManager.getConnection("jdbc:sqlite:test.db"),
connection -> {
try (var stmt = connection.createStatement();
var rs = stmt.executeQuery("SELECT COUNT(*) FROM users")) {
if (rs.next()) {
System.out.println("User count: " + rs.getInt(1));
}
}
}
);
}
}
2. Composite Resource Pattern
public class CompositeResourcePattern {
static class CompositeCloseable implements AutoCloseable {
private final List<AutoCloseable> resources;
public CompositeCloseable(AutoCloseable... resources) {
this.resources = Arrays.asList(resources);
}
@Override
public void close() throws Exception {
Exception firstException = null;
// Close in reverse order
for (int i = resources.size() - 1; i >= 0; i--) {
try {
resources.get(i).close();
} catch (Exception e) {
if (firstException == null) {
firstException = e;
} else {
firstException.addSuppressed(e);
}
}
}
if (firstException != null) {
throw firstException;
}
}
}
public static void compositeResourceUsage() throws Exception {
// Using composite pattern for multiple resources
try (var composite = new CompositeCloseable(
new FileInputStream("input.txt"),
new FileOutputStream("output.txt"),
new DatabaseConnection("jdbc:sqlite:test.db")
)) {
// All resources will be properly closed
System.out.println("Using composite resources");
}
}
}
3. Error Recovery Patterns
public class ErrorRecoveryPatterns {
public static void processWithFallback(String primaryPath, String fallbackPath) {
// Try primary resource, fallback to secondary on failure
try (var primary = openResource(primaryPath)) {
primary.process();
} catch (Exception primaryError) {
System.err.println("Primary resource failed: " + primaryError.getMessage());
try (var fallback = openResource(fallbackPath)) {
fallback.process();
} catch (Exception fallbackError) {
System.err.println("Fallback also failed: " + fallbackError.getMessage());
primaryError.addSuppressed(fallbackError);
throw new RuntimeException("All resources failed", primaryError);
}
}
}
public static void retryWithBackoff(ResourceFactory<?> factory, int maxRetries)
throws Exception {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try (var resource = factory.create()) {
// Use resource
return;
} catch (Exception e) {
if (attempt == maxRetries) {
throw e;
}
System.out.println("Attempt " + attempt + " failed, retrying...");
Thread.sleep(attempt * 1000L); // Exponential backoff
}
}
}
private static ProcessableResource openResource(String path) {
return new ProcessableResource(path);
}
static class ProcessableResource implements AutoCloseable {
private final String path;
public ProcessableResource(String path) {
this.path = path;
}
public void process() {
System.out.println("Processing: " + path);
}
@Override
public void close() {
System.out.println("Closing: " + path);
}
}
}
Common Pitfalls and Solutions
public class CommonPitfalls {
// 1. Don't reuse variables in try-with-resources
public static void avoidVariableReuse() {
// Wrong - variable reuse
// try (var stream = new FileInputStream("file1.txt")) {
// // use stream
// }
// try (stream = new FileInputStream("file2.txt")) { // Compilation error
// // use stream
// }
// Correct - separate variables
try (var stream1 = new FileInputStream("file1.txt")) {
// use stream1
}
try (var stream2 = new FileInputStream("file2.txt")) {
// use stream2
}
}
// 2. Handle null resources properly
public static void processWithNullableResource(String filename) throws IOException {
InputStream input = null;
try {
if (filename != null) {
input = new FileInputStream(filename);
// Process input
}
} finally {
if (input != null) {
input.close();
}
}
}
// 3. Be careful with resource ordering
public static void properResourceOrdering() throws IOException {
// Resources are closed in reverse order of declaration
try (var outer = new OuterResource();
var middle = new MiddleResource();
var inner = new InnerResource()) {
// Use resources
}
// Closing order: inner -> middle -> outer
}
static class OuterResource implements AutoCloseable {
public OuterResource() { System.out.println("Creating Outer"); }
public void close() { System.out.println("Closing Outer"); }
}
static class MiddleResource implements AutoCloseable {
public MiddleResource() { System.out.println("Creating Middle"); }
public void close() { System.out.println("Closing Middle"); }
}
static class InnerResource implements AutoCloseable {
public InnerResource() { System.out.println("Creating Inner"); }
public void close() { System.out.println("Closing Inner"); }
}
}
Pattern matching in try-with-resources makes resource management more concise and readable while maintaining all the benefits of automatic resource management. The key advantages include reduced boilerplate code, improved readability, and continued guaranteed resource cleanup.