Oracle Java Foundations study materials
Oracle Java Foundations study resources

Finally Blocks That Outvote Return Statements

Oracle Java Foundations · Put your knowledge to work in context.

All case studies

Finally Blocks That Outvote Return Statements

A utility class contains this method:

static int probe() {
    int x = 1;
    try {
        x = 2;
        throw new IllegalStateException("boom");
    } catch (IllegalStateException e) {
        x = 3;
        return x;
    } finally {
        x = 4;
    }
}

Callers get 3 even though the finally block ran last and left x at 4. A reviewer's variant puts return x; inside the finally block instead of the assignment, and the method then returns 4 while the thrown exception never surfaces. A third file will not compile: catch (Exception e) precedes catch (IllegalArgumentException e). The build also rejects catch (IOException | FileNotFoundException e).

1. Explain the return value 3 in the original method, then explain the 4-and-swallowed-exception behaviour of the variant.

Read the worked answer

When return x; executes in the catch block, the value of the expression is evaluated and saved (3). That pending return is not abandoned just because more code runs. The finally block then executes; x = 4; modifies only the local variable, which is irrelevant because the saved value is 3. Control then completes the pending return, so the method returns 3. The finally block always runs, but plain statements in it cannot change an already-computed return value.

In the variant, return x; inside finally completes abruptly. An abrupt completion of finally discards whatever the try/catch was doing - the saved return value and any in-flight exception. The block reads x as 4 and returns 4. Because the exception is discarded, callers see a normal return and the IllegalStateException disappears entirely, which hides failures. Using return, throw, or break out of finally is a defect, not an optimisation.

2. Why does `catch (Exception e)` before `catch (IllegalArgumentException e)` fail to compile, and why is the multi-catch form rejected?

Read the worked answer

Catch clauses are tested in order and the compiler enforces reachability. IllegalArgumentException is a subclass of Exception, so if the first clause is present it already handles every IllegalArgumentException; the second clause could never execute. Unreachable catch blocks are a compile-time error (exception ... has already been caught). The fix is to order the most specific type first:

catch (IllegalArgumentException e) { ... }
catch (Exception e) { ... }

The multi-catch catch (IOException | FileNotFoundException e) is rejected because multi-catch alternatives must be disjoint - no alternative may be a subclass of another. FileNotFoundException extends IOException, so the pair is illegal. Correct form: catch (IOException e), which covers both.

Two further multi-catch rules worth remembering: the parameter is implicitly final, so you cannot reassign e; and the compiler infers the least upper bound type, so only members common to all alternatives are callable.

3. The team wants to remove the catch block for a `NumberFormatException` raised by `Integer.parseInt`. Is that legal, and how is that different from a method declaring `throws IOException`?

Read the worked answer

Yes, legal. NumberFormatException extends IllegalArgumentException extends RuntimeException, so it is unchecked. The compiler does not require a catch or a throws clause for unchecked exceptions; they indicate programming errors (bad input to parseInt, null dereference, bad index) and propagate freely. Removing the catch compiles, but the exception will then escape the method at runtime and unwind to the caller - the program still terminates unless something higher up handles it.

A method declaring throws IOException throws a checked exception. A caller must either handle it with a catch that is compatible (here IOException or a supertype such as Exception), or declare the same exception in its own throws clause. Omitting both is a compile-time error, not a runtime surprise.

So: compiles without handling does not mean safe. The judgement call is whether the failure is recoverable at that point; parsing user input is usually recoverable, and swallowing it silently with an empty catch is the real defect.