Lesson 12 / 13
Try-with-Resources
When the body and the close fail at the same time, the hand-written form fails to reach one of the two events, while try-with-resources reaches both. The measurement shows, together, the count of lost and kept exceptions, closing's reverse order, and the extra five instructions the compiler generates.
Contents
In the previous lesson, every measurement had a single event: an exception got thrown, declared, and caught. In a real body, an event is often not alone. If a resource got opened, that resource has to be closed while the body is failing, and the close can fail too. In that case, two exceptions show up at once, and both have to go up the same path.
Releasing a resource with a context manager was built in the Python Fundamentals course; that mechanism is not repeated here. The question here is numeric: when two events show up, how many can be reached, which one becomes unreachable, and in what order does closing happen? This course’s question changes shape in this lesson: the party deciding is not a type, it is who wrote the code — a hand-written close and a compiler-generated close give separate results for the same body.
The Lost Exception and the Carried Exception
- GE21 — The resource class gets defined within the lesson; every open and close gets written to a log, and the log gets cleared before every measurement.
- GE22 — How many events each measurement produces is known from the setup and sits as the denominator in the table; reached count is counted from the escaped exception and the suppressed exceptions it carries.
- GE23 — Only the message text gets printed from the exceptions; the stack trace and location do not.
// Closing.java - hand-written closing next to try-with-resources
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Event extends Exception {
Event(String m) { super(m); }
}
class Resource implements AutoCloseable {
static final List<String> log = new ArrayList<>();
private final String name;
private final boolean failsOnClose;
Resource(String name, boolean failsOnClose) {
this.name = name;
this.failsOnClose = failsOnClose;
log.add("opened " + name);
}
@Override public void close() throws Event {
log.add("closed " + name);
if (failsOnClose) throw new Event("close " + name);
}
}
interface Trial {
void run() throws Event;
}
public class Closing {
static void manual() throws Event {
Resource r = new Resource("A", true);
try {
throw new Event("body");
} finally {
r.close();
}
}
static void tryWithResources() throws Event {
try (Resource r = new Resource("A", true)) {
throw new Event("body");
}
}
static void closeOnly() throws Event {
try (Resource r = new Resource("A", true)) {
Resource.log.add("body intact");
}
}
static void twoClosing() throws Event {
try (Resource a = new Resource("A", true); Resource b = new Resource("B", true)) {
Resource.log.add("body intact");
}
}
static void threeResources() throws Event {
try (Resource a = new Resource("A", false);
Resource b = new Resource("B", false);
Resource c = new Resource("C", false)) {
Resource.log.add("body intact");
}
}
static void report(String label, int events, Trial t) {
Resource.log.clear();
String escaped = "-";
int reached = 0;
try {
t.run();
} catch (Event e) {
escaped = e.getMessage();
reached = 1 + e.getSuppressed().length;
List<String> suppressed =
Arrays.stream(e.getSuppressed()).map(Throwable::getMessage).toList();
if (!suppressed.isEmpty()) escaped += " + " + suppressed;
}
System.out.printf("%-16s %-26s %-9s %s%n", label, escaped, reached + " / " + events,
Resource.log);
}
public static void main(String[] args) {
System.out.printf("%-16s %-26s %-9s %s%n", "measurement", "escaped (+ suppressed)",
"reached", "log");
report("manual", 2, Closing::manual);
report("try-with-resources", 2, Closing::tryWithResources);
report("close only", 1, Closing::closeOnly);
report("two closes", 2, Closing::twoClosing);
report("three resources", 0, Closing::threeResources);
}
}
measurement escaped (+ suppressed) reached log manual close A 1 / 2 [opened A, closed A] try-with-resources body + [close A] 2 / 2 [opened A, closed A] close only close A 1 / 1 [opened A, body intact, closed A] two closes close B + [close A] 2 / 2 [opened A, opened B, body intact, closed B, closed A] three resources - 0 / 0 [opened A, opened B, opened C, body intact, closed C, closed B, closed A]
The first two rows are this lesson’s measurement. Same body, same resource, same two events: one exception in the body, one in the close. In the hand-written form, the only exception reaching the caller is the close exception; the body’s exception stops nowhere and gets lost. Reached count is one of two. In try-with-resources, reached count is two of two: the escaped exception is the body’s, and the close exception gets carried as its suppressed exception.
The reason for the loss is in the meaning of finally. If an
exception comes out of finally, it replaces the exception already
on its way; two exceptions cannot travel up at once, and the
hand-written form of the language has no place to keep the second one.
Try-with-resources opens exactly that place: the body’s exception stays
on the way, and the close’s exception gets attached to it.
The measured cost of the loss is which event the defect shows up as. In the hand-written form, the caller sees the message “close A”; the actual event that stopped the program, though, is in the body, and that event leaves no trace anywhere. A resource that fails while closing covers everything that failed before it.
Closing Order
The third, fourth, and fifth rows’ logs write out closing’s order. In
the three-resource measurement, opening order is A, B, C, closing
order is C, B, A. Resources get closed in the reverse order of
opening, and this is not arbitrary: a resource opened later may depend
on one opened earlier, never the other way around.
The fourth row shows the same rule together with two events. A and
B get opened, the body finishes intact, both fail while closing. The
exception reaching the caller is B’s close exception — that is, the
one closed first, the one opened later. A’s gets written as
its suppressed exception. Order decides which one comes out here too:
whichever exception is first on the way, the rest get attached to it.
In the fifth row, no resource fails while closing, and reached event count is zero; the log still shows three opens and three closes. Closing happens in every case, whether there is an exception or not.
The Declared Type Decides Eligibility as a Resource
- GE24 — The three parts of the Scale core used in this lesson get written: compiling, a method’s instruction count, and the names of the methods it calls.
- GE25 — Across the six attempts, the types and bodies are fixed; only the type the resource gets declared with, the line in the body, and the enclosing method’s declaration change.
// Scale.java - shared measurement core: compiles a class file and reads it
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.InvokeInstruction;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Scale {
static ClassModel compile(String source, String className) throws Exception {
Path dir = Files.createTempDirectory("scale");
Path file = dir.resolve(className + ".java");
Files.writeString(file, source);
PrintWriter silent = new PrintWriter(Writer.nullWriter());
if (ToolProvider.findFirst("javac").orElseThrow()
.run(silent, silent, "-d", dir.toString(), file.toString()) != 0)
throw new IllegalStateException("did not compile: " + className);
return ClassFile.of().parse(dir.resolve(className + ".class"));
}
static int instructionCount(String source, String className, String methodName) throws Exception {
int count = 0;
for (MethodModel m : compile(source, className).methods())
if (m.methodName().stringValue().equals(methodName))
for (CodeElement e : m.code().orElseThrow())
if (e instanceof Instruction) count++;
return count;
}
static List<String> calls(String source, String className, String methodName) throws Exception {
List<String> names = new ArrayList<>();
for (MethodModel m : compile(source, className).methods())
if (m.methodName().stringValue().equals(methodName))
for (CodeElement e : m.code().orElseThrow())
if (e instanceof InvokeInstruction iv)
names.add(shortName(iv.owner().asInternalName()) + "." + iv.name().stringValue());
return names;
}
static String shortName(String internalName) { return internalName.substring(internalName.lastIndexOf('/') + 1); }
}
// Eligible.java - when can a name be a resource
public class Eligible {
static final String TYPES = """
class Event extends Exception { Event(String m) { super(m); } }
class Closing implements AutoCloseable { @Override public void close() throws Event { } }
class Silent implements AutoCloseable { @Override public void close() { } }
class NotClosing { public void close() { } }
""";
static final String DECLARING = TYPES + "class C { static void y() throws Event { %s } }";
static final String NOT_DECLARING = TYPES + "class C { static void y() { %s } }";
static void attempt(String pattern, String label, String body) {
try {
Scale.compile(pattern.formatted(body), "C");
System.out.printf("%-42s -> compiled%n", label);
} catch (Exception e) {
System.out.printf("%-42s -> did not compile%n", label);
}
}
public static void main(String[] args) {
System.out.println("enclosing method declares Event");
attempt(DECLARING, "try (Closing k = new Closing())", "try (Closing k = new Closing()) { }");
attempt(DECLARING, "try (Object k = new Closing())", "try (Object k = new Closing()) { }");
attempt(DECLARING, "try (NotClosing k = new NotClosing())",
"try (NotClosing k = new NotClosing()) { }");
attempt(DECLARING, "reassigned inside body k = new Closing()",
"try (Closing k = new Closing()) { k = new Closing(); }");
System.out.println();
System.out.println("enclosing method declares nothing");
attempt(NOT_DECLARING, "try (Closing k = new Closing())", "try (Closing k = new Closing()) { }");
attempt(NOT_DECLARING, "try (Silent k = new Silent())", "try (Silent k = new Silent()) { }");
}
}
enclosing method declares Event try (Closing k = new Closing()) -> compiled try (Object k = new Closing()) -> did not compile try (NotClosing k = new NotClosing()) -> did not compile reassigned inside body k = new Closing() -> did not compile enclosing method declares nothing try (Closing k = new Closing()) -> did not compile try (Silent k = new Silent()) -> compiled
The second row ties this lesson to the course’s axis. The same object
— runtime type Closing, that is, a closable object — cannot be a
resource once named as Object. Even though the object is closable,
the compiler does not generate the close call, because it looks at the
declared type to decide whether a call to generate exists at all.
Try-with-resources’s entire mechanism gets built at compile time, and
the object’s actual class never gets asked.
The third row is the same rule’s lower bound: a class that has a
method named close but does not take on the closability
contract cannot be a resource. The name is not enough; what is
needed is the declared type carrying that contract.
The fourth row shows the resource variable cannot get rebound inside the body. The reasoning for this is the generated code itself: the compiler generates the close call for the object bound at the start, and if the variable could be rebound, the object closed and the object opened would come apart.
The last two rows are the previous lesson’s narrowing rule’s
counterpart here. When the close method declares a checked
exception, the try-with-resources block carries that exception to the
caller, and the enclosing method has to make a choice. The same block
produces no requirement when close declares nothing. The cost
try-with-resources writes to the caller matches the resource’s close
declaration exactly.
The Code the Compiler Generates
- GE26 — The measured source gets written within the lesson; both methods’ bodies do the same work, only the way the close gets written changes.
// Generated.java - what try-with-resources produces in the class file
public class Generated {
static final String SOURCE = """
class Event extends Exception { Event(String m) { super(m); } }
class Resource implements AutoCloseable {
void work() { }
@Override public void close() throws Event { }
}
class C {
static void manual() throws Event {
Resource r = new Resource();
try { r.work(); } finally { r.close(); }
}
static void tryWithResources() throws Event {
try (Resource r = new Resource()) { r.work(); }
}
}
""";
public static void main(String[] args) throws Exception {
for (String y : new String[] {"manual", "tryWithResources"})
System.out.printf("%-18s instructions %2d calls %s%n", y,
Scale.instructionCount(SOURCE, "C", y), Scale.calls(SOURCE, "C", y));
}
}
manual instructions 15 calls [Resource.<init>, Resource.work, Resource.close, Resource.close] tryWithResources instructions 20 calls [Resource.<init>, Resource.work, Resource.close, Resource.close, Throwable.addSuppressed]
The form that is two lines shorter in the source is five instructions longer in the class file: the hand-written version is 15, the try-with-resources version 20. Try-with-resources is a shortening, but what it shortens is the written text; the code it generates does extra work.
Two close calls show up in the call lists: one for the path that
runs when the body finishes cleanly, one for the path that runs when
the body fails. Both of these exist in the hand-written form too,
because the compiler places the finally block on both paths. What
makes the difference is the fifth call, showing up only in
try-with-resources: the suppression call. The extra five
instructions belong to this too — holding the exception escaping the
body, trying the close on a separate branch, and attaching the second
exception to the first.
What the measurement says: a suppressed exception is not something that happens on its own at the language’s runtime. There is a call, inside the code closing the resource, that asks for suppression, and the compiler put that call there; someone writing the same call by hand would get the same result. What try-with-resources earns is not a new capability, it is that code getting written every time, and in the same way.
A Closing Exception Does Not Suppress the Body’s
This lesson’s boundary measurement sits in comparing the second and third rows, and shows that suppression’s direction runs one way.
In the second row, both the body and the close fail, and the escaped exception is the body’s; the close’s gets carried as suppressed. The direction never reverses: an exception failing during close cannot suppress the body’s, because the body’s exception is already on the way and gives its place to no one.
The third row draws this rule’s boundary. When the body finishes intact, there is nothing to suppress; the close exception comes out directly, and its suppressed list is empty. Suppression, that is, is not a hiding mechanism, it is only a place carrying a second event. A defect in closing loses nothing of its visibility when it is alone.
The rule these two rows say together: try-with-resources destroys no event and changes no event’s priority. The exception lost in the hand-written form was lost not because the priority order was wrong, but because there was no place to put a second event.
Summary
- When the body and the close fail at the same time, only one of two events gets reached in the hand-written form; both get reached with try-with-resources.
- In the hand-written form, an exception coming out of
finallyreplaces the one on its way, and no trace of the body’s event remains; the caller sees the close event, not the event that stopped the program. - Resources get closed in the reverse order of opening; when more than one close fails, the first one closed’s exception comes out, and the rest get written to it as suppressed.
- The declared type decides whether a name can be a resource: a
closable object does not get closed once named as
Object, and the block writes to the caller only as much cost as the close declaration carries. - Try-with-resources is five instructions longer than the hand-written form in the class file; what shortens is the written text, not the generated code.
- Boundary measurement: suppression’s direction runs one way. A closing exception does not suppress the body’s, only the reverse happens; when the body finishes intact, the closing exception comes out directly and alone.
Next Step
Across three lessons, an exception was always a given: it got thrown, declared, caught, suppressed. None of them asked whether an event should be reported as an exception at all. Yet the same work can be written with three separate contracts: a checked exception, an unchecked exception, or no exception at all, returning the result through a type. This course’s last lesson builds all three around the same work, counts how many decision points the compiler forces on the calling side, and measures where a defect shows up once a contract breaks.
To keep your progress and take notes, Log in
My notes
Log in to take notes.