🃏 Try-With-Resources and Suppressed Exceptions
The exception in the try block is primary. Exceptions thrown by close()
are suppressed and attached to the primary exception.
class MyResource implements AutoCloseable {
private String name;
MyResource(String name) { this.name = name; }
void doWork() throws Exception {
throw new RuntimeException("Work failed in " + name);
}
@Override
public void close() throws Exception {
throw new RuntimeException("Close failed for " + name);
}
}
// Example usage:
try (MyResource res = new MyResource("Database")) {
res.doWork(); // Throws primary exception
// close() will be called automatically and its exception suppressed
} catch (Exception e) {
System.out.println("Primary: " + e.getMessage()); // Work failed in Database
// Check suppressed exceptions:
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Suppressed: " + suppressed.getMessage()); // Close failed for Database
}
}
Multiple resources example:
try (MyResource r1 = new MyResource("DB1");
MyResource r2 = new MyResource("DB2")) {
// Resources closed in reverse order: r2.close(), then r1.close()
throw new RuntimeException("Business logic error");
} catch (Exception e) {
// Primary: Business logic error
// Suppressed: Close failed for DB2, Close failed for DB1
}
💡 Learning Tip: Primary exception is the “star of the show” - suppressed exceptions are the “supporting cast.”
🃏 Stream Operations and Exception Handling
Intermediate vs Terminal Operations:
// Intermediate Operations - return Stream (lazy):
Stream<String> words = Stream.of("apple", "banana", "cherry");
Stream<String> processed = words
.filter(s -> s.startsWith("a")) // Intermediate
.map(String::toUpperCase) // Intermediate
.limit(2); // Intermediate
// Nothing executed yet - streams are lazy!
// Terminal Operations - return result and close stream:
List<String> result = Stream.of("apple", "banana", "cherry")
.filter(s -> s.length() > 5) // Intermediate
.collect(Collectors.toList()); // Terminal - execution happens here
Stream Reuse Error:
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::print); // Terminal operation - stream is consumed
// stream.count(); // ❌ IllegalStateException: stream has already been operated upon
Optional Exception Handling:
private static void demonstrateOptionalExceptions() {
// Safe stream that produces a result:
Stream<Integer> numbers = Stream.of(1, 3, 7, 2, 8);
Optional<Integer> maxOpt = numbers
.filter(x -> x < 10) // All numbers pass
.max(Integer::compareTo); // Find max: Optional[8]
System.out.println(maxOpt.get()); // ✅ 8 - safe because Optional has value
// Dangerous stream that produces empty Optional:
Stream<Integer> emptyStream = Stream.of(15, 20, 25);
Optional<Integer> emptyOpt = emptyStream
.filter(x -> x < 5) // No numbers pass filter
.max(Integer::compareTo); // Returns Optional.empty()
// System.out.println(emptyOpt.get()); // ❌ NoSuchElementException!
// Safe alternatives:
System.out.println(emptyOpt.orElse(-1)); // -1 (default value)
System.out.println(emptyOpt.orElseGet(() -> 0)); // 0 (computed default)
emptyOpt.ifPresent(System.out::println); // Does nothing if empty
}
💡 Learning Tips:
- Intermediate = “keep the pipeline flowing”, Terminal = “time for results”
- Optional.get() = “Russian roulette” - always check isPresent() or use orElse()/ifPresent()
🃏 Exception Output Methods
Rule: Exception output methods provide different levels of detail for debugging.
- System.out.println(exception): Prints only exception class name and message.
- exception.printStackTrace(): Prints complete method call chain with line numbers.
- Stack trace shows the full path from thread start to where exception was created.
class Parent {
void callChild() {
Child child = new Child();
child.processFamily(); // Line 4
}
}
class Child {
void processFamily() {
throw new RuntimeException("Family processing failed"); // Line 9
}
}
public class FamilyApp {
public static void main(String[] args) {
try {
Parent parent = new Parent();
parent.callChild(); // Line 15
} catch (RuntimeException e) {
System.out.println(e); // Output: java.lang.RuntimeException: Family processing failed
System.out.println("---");
e.printStackTrace(); // Output: Full stack trace with method names and line numbers
/*
java.lang.RuntimeException: Family processing failed
at Child.processFamily(Child.java:9)
at Parent.callChild(Parent.java:4)
at FamilyApp.main(FamilyApp.java:15)
*/
}
}
}
💡 Learning Tip: Remember “PRINT vs TRACE” - println() gives you the message, printStackTrace() gives you the journey.
Q: What’s the difference between printing an exception and calling printStackTrace()?
A: println() shows only class name and message, printStackTrace() shows the complete method call chain with line numbers back to thread start.
🃏 Multi-Catch and Try-With-Resources Exception Flow
⚠️ This code has a COMPILE ERROR! Let’s analyze why and fix it:
5: public static void main(String... unused) {
6: System.out.print("1"); // Always executes first
7: try (StringBuilder resource1 = new StringBuilder()) {
8: System.out.print("2"); // Executes in try block
9: throw new IllegalArgumentException(); // Throws RuntimeException
10: } catch (Exception error1 | RuntimeException error2) { // ❌ COMPILE ERROR!
11: System.out.print("3");
12: throw new FileNotFoundException();
13: } finally {
14: System.out.print("4"); // Always executes
15: } }
🚫 Compilation Error: Line 10 is invalid because RuntimeException
is a subclass of Exception
. In multi-catch, you cannot have a subclass and superclass in the same statement.
✅ Fixed Version 1 - Remove redundant RuntimeException:
5: public static void main(String... unused) throws Exception {
6: System.out.print("1"); // Step 1: Print "1"
7: try (StringBuilder resource1 = new StringBuilder()) {
8: System.out.print("2"); // Step 2: Print "2"
9: throw new IllegalArgumentException(); // Step 3: Throw RuntimeException
10: } catch (Exception error1) { // Step 4: Catch Exception (includes RuntimeException)
11: System.out.print("3"); // Step 5: Print "3"
12: throw new FileNotFoundException(); // Step 6: Throw new exception
13: } finally {
14: System.out.print("4"); // Step 7: Always print "4"
15: } }
// Output: "1234" then FileNotFoundException is thrown
✅ Fixed Version 2 - Separate catch blocks:
5: public static void main(String... unused) throws Exception {
6: System.out.print("1"); // Step 1: Print "1"
7: try (StringBuilder resource1 = new StringBuilder()) {
8: System.out.print("2"); // Step 2: Print "2"
9: throw new IllegalArgumentException(); // Step 3: Throw RuntimeException
10: } catch (RuntimeException error1) { // Step 4: Catch RuntimeException first
11: System.out.print("3"); // Step 5: Print "3"
12: throw new FileNotFoundException(); // Step 6: Throw new exception
13: } catch (Exception error2) { // This would catch other Exceptions
14: System.out.print("5"); // Won't execute (RuntimeException caught above)
15: } finally {
16: System.out.print("4"); // Step 7: Always print "4"
17: } }
// Output: "1234" then FileNotFoundException is thrown
Exception Flow Analysis:
// Execution order breakdown:
// 1. Line 6: Print "1"
// 2. Line 7: StringBuilder resource created (no exception in creation)
// 3. Line 8: Print "2"
// 4. Line 9: IllegalArgumentException thrown
// 5. Line 10: Exception caught (if fixed)
// 6. Line 11: Print "3"
// 7. Line 12: FileNotFoundException thrown
// 8. Line 14: Finally block executes, print "4"
// 9. FileNotFoundException propagates up (method must declare throws Exception)
// Note: StringBuilder.close() is called automatically but does nothing
// (StringBuilder implements AutoCloseable but close() is empty)
Key Learning Points:
Multi-catch rules:
// ❌ Invalid - subclass and superclass together:
catch (Exception e1 | RuntimeException e2) { }
catch (IOException e1 | FileNotFoundException e2) { }
// ✅ Valid - same level exceptions:
catch (IOException e1 | SQLException e2) { }
catch (IllegalArgumentException e1 | IllegalStateException e2) { }
// ✅ Valid - single variable name:
catch (IOException | SQLException error) { } // Same variable name
Try-with-resources execution order:
try (Resource1 res1 = new Resource1();
Resource2 res2 = new Resource2()) {
// try block code
} catch (Exception e) {
// exception handling
} finally {
// finally block
}
// Execution order:
// 1. Create res1
// 2. Create res2
// 3. Execute try block
// 4. If exception: close res2, then close res1 (reverse order)
// 5. Handle exception in catch
// 6. Execute finally block
// 7. Propagate any uncaught exceptions
💡 Learning Tips:
- Multi-catch rule: “No family hierarchy” - can’t catch parent and child in same statement
- Finally guarantee: “Finally always runs” - even when exceptions thrown in catch blocks
- Resource cleanup: “LIFO cleanup” - resources closed in reverse creation order
- Exception propagation: “New exceptions replace old ones” - FileNotFoundException replaces IllegalArgumentException
Common Exam Traps:
- Multi-catch with inheritance hierarchy - Always compile error
- Finally block execution - Runs even when catch throws new exception
- Exception masking - New exception in catch/finally masks original
- Resource closing order - Always reverse of creation order