3

I was wondering, when exception occurs how try with resource statement manages to close resources before entering catch block. When exception occurs execution immediately jumps to catch block. So where actually try-with-resource closes the resources.

To get better understanding of how it works I decided to see how compiler implements it. I wrote following code and and compiled it.

public class Test
{
    public static void main(final String[] args) {
       //I used same JDK for compilation and execution.
        System.out.println("Java version: " + System.getProperty("java.version") + "\n");
        try(CloseMe me = new CloseMe(); 
                CloseMeToo meToo = new CloseMeToo()){
                
                System.out.println("trying");
                throw new Exception("try failed");

            } catch(Exception e) {
                System.out.println("failed");
                System.out.println("\n");
                System.out.println(e.getMessage());
                System.out.println(e.getSuppressed()[0].getMessage());
                System.out.println(e.getSuppressed()[1].getMessage());
            }
    }
}

class CloseMe implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("me closing!");
        throw new Exception("don't close me :o");
    }
}

class CloseMeToo implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("meToo closing!");
        throw new Exception("don't close me too :O");
    }
}

Output

Java version: 15.0.1

trying
meToo closing!
me closing!
failed


try failed
don't close me too :O
don't close me :o

Then I went to www.javadecompilers.com and tried the decompilers there. Two decompilers gave decent results: CFR 0.150 and Fernflower. CFR is most readable and complete so posting it here.

public class Test
{
    public static void main(final String[] args) throws Throwable{
        System.out.println("Java version: " + System.getProperty("java.version") + "\n");
        
        try {
            Throwable throwable = null;
            Object var2_4 = null;     //<-- where this variable is used?
            try {
                CloseMe me = new CloseMe();
                try {
                    CloseMeToo meToo = new CloseMeToo();
                    try {
                        System.out.println("trying");
                        throw new Exception("try failed");
                    }
                    catch (Throwable throwable2) {
                        throwable = throwable2; //<-- I put this line to make it work
                        if (meToo != null) {
                            meToo.close();
                        }
                        throw throwable2;
                    }
                }
                catch (Throwable throwable3) {
                    if (throwable == null) {
                        throwable = throwable3;
                    } else if (throwable != throwable3) {
                        throwable.addSuppressed(throwable3);
                    }
                    if (me != null) {
                        me.close();
                    }
                    throw throwable;
                }
            }
            catch (Throwable throwable4) {
                if (throwable == null) {
                    throwable = throwable4;
                } else if (throwable != throwable4) {
                    throwable.addSuppressed(throwable4);
                }
                throw throwable;
            }
        }
        catch (Exception e) {
            System.out.println("failed");
            System.out.println("\n");
            System.out.println(e.getMessage());
            System.out.println(e.getSuppressed()[0].getMessage());
            System.out.println(e.getSuppressed()[1].getMessage());
        }
    }
}

I understand decompilers have limitations. Ideal decompiler would've given me same try-with-resource back and I wouldn't have seen these details. So it is ok.
My questions are:

  1. In above decompiled code, variable var2_4 is unused. Also, I had to add a line to make it work like try-with-resource. I think the code is not complete. If there are any, can you add/explain missing parts?
  2. If anyone, who understands bytecode, translate class file to exact java code would be great. Or give me pointers about where can I get the tools to do the job.

Thank you!

onkar ruikar
  • 1,641
  • 1
  • 4
  • 14
  • The compiler already provided a basic version of what the compiler is translating the try-with-resources into. Shouldn't that along with the specification already be sufficient? – Thomas Mar 30 '21 at 13:44
  • 1
    https://stackoverflow.com/a/17356707/1420279 – Antimony Mar 30 '21 at 14:25

1 Answers1

6

The behavior of try-with-resources is fully documented in the Java Language Specification, section 14.20.3. try-with-resources.

It specifically shows that the following abbreviated version of the question code:

try (CloseMe me = new CloseMe(); CloseMeToo meToo = new CloseMeToo()) {
    System.out.println("trying");
} catch (Exception e) {
    System.out.println("failed");
}

first gets converted to:

try {
    try (CloseMe me = new CloseMe(); CloseMeToo meToo = new CloseMeToo()) {
        System.out.println("trying");
    }
} catch (Exception e) {
    System.out.println("failed");
}

then to:

try {
    final CloseMe me = new CloseMe();
    Throwable #primaryExc1 = null;

    try (CloseMeToo meToo = new CloseMeToo()) {
        System.out.println("trying");
    } catch (Throwable #t) {
        #primaryExc1 = #t;
        throw #t;
    } finally {
        if (me != null) {
            if (#primaryExc1 != null) {
                try {
                    me.close();
                } catch (Throwable #suppressedExc) {
                    #primaryExc1.addSuppressed(#suppressedExc);
                }
            } else {
                me.close();
            }
        }
    }
} catch (Exception e) {
    System.out.println("failed");
}

then to:

try {
    final CloseMe me = new CloseMe();
    Throwable #primaryExc1 = null;

    try {
        final CloseMeToo meToo = new CloseMeToo()
        Throwable #primaryExc2 = null;

        try {
            System.out.println("trying");
        catch (Throwable #t) {
            #primaryExc2 = #t;
            throw #t;
        } finally {
            if (meToo != null) {
                if (#primaryExc2 != null) {
                    try {
                        meToo.close();
                    } catch (Throwable #suppressedExc) {
                        #primaryExc2.addSuppressed(#suppressedExc);
                    }
                } else {
                    meToo.close();
                }
            }
        }
    } catch (Throwable #t) {
        #primaryExc1 = #t;
        throw #t;
    } finally {
        if (me != null) {
            if (#primaryExc1 != null) {
                try {
                    me.close();
                } catch (Throwable #suppressedExc) {
                    #primaryExc1.addSuppressed(#suppressedExc);
                }
            } else {
                me.close();
            }
        }
    }
} catch (Exception e) {
    System.out.println("failed");
}
Andreas
  • 138,167
  • 8
  • 112
  • 195
  • Note that the actual compiler implementation uses gotos that can't be represented at the language level. – Antimony Mar 30 '21 at 14:24