This is an attempt to answer my own question, but it uses the experiment and the results of what comes from the Java compiler, so it does not particularly concern philosophy or anything like that.
Here is the sample code for catch-cleanup-and-rethrow:
public CompoundResource catchThrowable() throws Exception { InputStream stream1 = null; InputStream stream2 = null; try { stream1 = new FileInputStream("1"); stream2 = new FileInputStream("2"); return new CompoundResource(stream1, stream2); } catch (Throwable t) { if (stream2 != null) { stream2.close(); } if (stream1 != null) { stream1.close(); } throw t; } }
This is compiling to the following bytecode:
public Exceptions$CompoundResource catchThrowable() throws java.lang.Exception; Code: 0: aconst_null 1: astore_1 2: aconst_null 3: astore_2 4: new
The following is some code for check-for-failure-in-finally-and-cleanup with different semantics:
public CompoundResource finallyHack() throws Exception { InputStream stream1 = null; InputStream stream2 = null; boolean success = false; try { stream1 = new FileInputStream("1"); stream2 = new FileInputStream("2"); success = true; return new CompoundResource(stream1, stream2); } finally { if (!success) { if (stream2 != null) { stream2.close(); } if (stream1 != null) { stream1.close(); } } } }
This compiles as follows:
public Exceptions$CompoundResource finallyHack() throws java.lang.Exception; Code: 0: aconst_null 1: astore_1 2: aconst_null 3: astore_2 4: iconst_0 5: istore_3 6: new
By carefully examining what happens here, it seems to generate the same bytecode, as if you were duplicating the entire finally block both at the return point and inside the catch block. In other words, it is as if you wrote this:
public CompoundResource finallyHack() throws Exception { InputStream stream1 = null; InputStream stream2 = null; boolean success = false; try { stream1 = new FileInputStream("1"); stream2 = new FileInputStream("2"); success = true; CompoundResource result = new CompoundResource(stream1, stream2); if (!success) { if (stream2 != null) { stream2.close(); } if (stream1 != null) { stream1.close(); } } return result; } catch (any t) {
If someone really wrote this code, you would laugh at them. In the success branch, success is always true, so there is a large piece of code that never runs, so you generate byte code that never runs, serving only to inflate your class file. In the exception branch, success is always incorrect, so you do an unnecessary check of the value before performing the cleanup, which, as you know, should happen, which again adds the size of the class file.
The most important thing to note:
Both catch (Throwable) and finally solutions actually catch all exceptions.
So, to answer the question: “Can I catch a Throwable to perform a cleanup?” ...
I'm still not sure, but I know that if this is not normal, catch Throwable for it, it is not normal to use finally for it. And if finally also not OK, what remains?