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.