I've written seven test cases for understanding the behavior of the finally
block. What is the logic behind how finally
works?
package core;
public class Test {
public static void main(String[] args) {
new Test().testFinally();
}
public void testFinally() {
System.out.println("One = " + tryOne());
System.out.println("Two = " + tryTwo());
System.out.println("Three = " + tryThree());
System.out.println("Four = " + tryFour());
System.out.println("Five = " + tryFive());
System.out.println("Six = " + trySix());
System.out.println("Seven = " + trySeven());
}
protected StringBuilder tryOne() {
StringBuilder builder = new StringBuilder();
try {
builder.append("Cool");
return builder.append("Return");
}
finally {
builder = null;
}
}
protected String tryTwo() {
String builder = "Cool";
try {
return builder += "Return";
}
finally {
builder = null;
}
}
protected int tryThree() {
int builder = 99;
try {
return builder += 1;
}
finally {
builder = 0;
}
}
protected StringBuilder tryFour() {
StringBuilder builder = new StringBuilder();
try {
builder.append("Cool");
return builder.append("Return");
}
finally {
builder.append("+1");
}
}
protected int tryFive() {
int count = 0;
try {
count = 99;
}
finally {
count++;
}
return count;
}
protected int trySix() {
int count = 0;
try {
count = 99;
}
finally {
count = 1;
}
return count;
}
protected int trySeven() {
int count = 0;
try {
count = 99;
return count;
}
finally {
count++;
}
}
}
Why builder = null
is not working?
Why does builder.append("+1")
work whereas count++
( in trySeven()) does not work?
Consider what the compiler is actually doing for the return statement, for instance in tryOne(): it copies a reference to
builder
back to the calling function's environment. After it's done this, but before control goes back to the calling function, the finally block executes. So you have something more like this, in practice:Or, in terms of the order that statements actually get executed (ignoring possible exceptions, of course), it looks more like this:
So setting
builder = null
does run, it just doesn't do anything useful. However, runningbuilder.append("something")
will have a visible effect, since both temp and builder refer to the same (mutable) object.Likewise, what's really happening in trySeven() is something more like this:
In this case, since we're dealing with an int, the copies are independent, so incrementing one doesn't affect the other.
All that said, the fact remains that putting return statements in a try-finally block is quite clearly confusing, so if you've got any kind of choice in the matter, you'd be better off rewriting things so that all your return statements are outside any try-finally blocks.
builder = null
andbuilder.append("+1")
are working. It's just that they're not affecting what you're returning. The function returns what thereturn
statement has, regardless of what happens afterward.The reason there is a difference is because
builder
is passed by reference.builder=null
changes the local copy ofbuilder
.builder.append("+1")
affects the copy held by the parent.The finally block is executed when you leave the try block. The "return" statement does two things, one it sets the return value of the function and two it exits the function. Normally this would look like an atomic operation but within a try block it will cause the finally block to execute after the return value was set and before the function exits.
Return execution:
Example one (primitive):
Example two(reference):
Example three (reference):
Example four (return):
Let's start with use case you'll see more often - you have a resource that you must close to avoid a leak.
In this case, we have to close the statement when we're done, so we don't leak database resources. This will ensure that in the case of an Exception being thrown, we will always close our Statement before the function exits.
try { ... } finally { ... } blocks are meant for ensuring that something will always execute when the method terminates. It's most useful for Exception cases. If you find yourself doing something like this:
You're not really using finally properly. There is a performance penalty to this. Stick to using it when you have Exception cases that you must clean up from. Try refactoring the above to this:
Why
builder = null
is not working?Because you are setting the local reference to null which will not change the content of the memory. So it is working, if you try to access the builder after finally block then you'll get null.
Why
builder.append("+1") work?
Because you are modifying the content of the memory using the reference,that's why it should work.
Why
count++
does not work in testFive()?It is working fine with me. It outputs 100 as expected.
Once you do the return, the only way to override that is to do another return (as discussed at Returning from a finally block in Java, this is almost always a bad idea), or otherwise complete abruptly. Your tests don't ever return from a finally.
JLS §14.1 defines abrupt completion. One of the abrupt completion types is a return. The try blocks in 1,2,3,4, and 7 abruptly complete due to returns. As explained by §14.20.2, if the try block completes abruptly for a reason R besides a throw, the finally block is immediately executed.
If the finally block completes normally (which implies no return, among other things), "the try statement completes abruptly for reason R.". In other words, the return initiated by the try is left intact; this applies to all your tests. If you return from the finally, "the try statement completes abruptly for reason S (and reason R is discarded)." (S here being the new overriding return).
So in tryOne, if you did:
this new return S would override the original return R.
For
builder.append("+1")
intryFour
, keep in mind StringBuilder is mutable, so you're still returning a reference to the same object specified in the try. You're just doing a last minute mutation.tryFive
andtrySix
are straight-forward. Since there is no return in the try, the try and finally both complete normally, and it executes the same as if there was no try-finally.