Skip to content
academia.sh

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 finally replaces 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.

Start typing to search.

↑↓ Esc navigate · open · close