finally 与 return 执行顺序

面试官问:"try-catch-finally 中,finally 和 return 哪个先执行?"

候选人小周答:"finally 先执行,然后 return。"

面试官写了一段代码:

public int test() {
    try {
        return 1;
    } finally {
        System.out.println("finally");
    }
}

"输出什么?"

小周:"1 和 finally。"

面试官又写了一段:

public int test() {
    int x = 1;
    try {
        return x;
    } finally {
        x = 2;
    }
}

"返回什么?"

小周:"1?"

面试官:"还是 1。"

小周困惑了:"finally 不是执行了吗?x 不是改成 2 了吗?"

【面试官心理】 这道题能精准区分"背过结论"和"真正理解过字节码"的候选人。知道"JVM 保存返回值副本"的人,才能真正理解为什么 finally 修改变量无效。

一、finally 的执行时机 🔴

1.1 正常流程

finally 总是在 try 或 catch 的 return 之前执行:

public int test() {
    try {
        System.out.println("try");
        return 1;
    } finally {
        System.out.println("finally"); // 先执行
    }
    // return 1; // 实际 return 在这里,finally 之后
}

输出:tryfinally

1.2 return 值覆盖原理

回到小周的困惑:

public int test() {
    int x = 1;
    try {
        return x; // JVM 将 x 的值(1)保存到"返回值寄存器"
    } finally {
        x = 2;    // 修改的是 x,但返回值已经保存了
    }
    // return 1; // 返回的是保存的值,而不是 x 的当前值
}

字节码层面的真相

iload_1        // 将 x (1) 压入操作数栈
istore_2       // 将栈顶值(1)保存到"返回值槽"(局部变量表 slot 2)
// try 块结束
// 执行 finally
iconst_2       // 将 2 压入栈
istore_1       // 将 2 保存到 x(局部变量表 slot 1)
iload_2        // 从返回值槽加载返回值(1)← 关键!
ireturn        // 返回

finally 修改的是局部变量 x,但返回值在 try 块 return 之前就已经保存了。

⚠️

finally 中修改变量不会影响 return 值,因为 return 值在 try 块执行时就被保存了。如果想通过 finally 修改返回值,应该用数组或对象包装。

二、finally 不一定执行的场景 🔴

2.1 System.exit()

try {
    System.out.println("try");
    System.exit(0); // JVM 直接退出
} finally {
    System.out.println("finally"); // 不会执行!
}

System.exit() 会终止 JVM 进程,finally 根本没机会执行。

2.2 OOM/StackOverflow 导致 JVM 崩溃

try {
    // 触发 StackOverflowError
    recursiveMethod();
} finally {
    System.out.println("finally"); // 可能不会执行
}

StackOverflowError 发生时,JVM 的栈空间已经严重不足,执行 finally 可能再次触发 StackOverflowError,导致 finally 无法完成。

2.3 守护线程中的 finally

Thread daemon = new Thread(() -> {
    try {
        // ...
    } finally {
        System.out.println("daemon finally"); // 如果主线程退出,JVM 直接退出
    }
});
daemon.setDaemon(true);
daemon.start();

当所有非守护线程结束后,JVM 会直接退出,守护线程的 finally 可能不会执行。

2.4 CPU 断电/硬件故障

这是最极端的情况,但理论上 JVM 进程被强制终止时,finally 不执行。

三、return 值类型的影响 🔴

3.1 返回对象引用时

public StringBuilder test() {
    StringBuilder sb = new StringBuilder("hello");
    try {
        return sb; // 保存的是引用(指向 sb 对象)
    } finally {
        sb.append(" world"); // 通过引用修改了对象内容
        // 但 return 的是同一个引用
    }
}

// 返回值:"hello world"

finally 修改的是对象的内部状态(通过引用),而不是引用本身。return 的引用指向的对象已经被修改了。

💡

这个区别非常关键:基本类型修改返回值不影响(传值);引用类型修改内部状态会影响返回结果(传引用)。

3.2 try-finally 没有 catch

public int test() {
    try {
        return 1;
    } finally {
        System.out.println("finally");
    }
}

// 编译通过,finally 正常执行
// 如果 try 中有异常,finally 也会执行

四、追问升级

面试官:"finally 中有 return 会怎样?"

public int test() {
    try {
        return 1;
    } finally {
        return 2; // finally 中的 return!
    }
}
// 编译通过,返回值:2(覆盖了 try 的 return)

这是极其糟糕的写法!finally 中的 return 会覆盖 try 中的 return,且 try 中的异常也会被静默丢弃

public int test() {
    try {
        throw new RuntimeException("error"); // 异常被丢弃
    } finally {
        return 0; // 正常返回,无异常
    }
}
// 调用方:返回 0,没有异常抛出!
⚠️

禁止在 finally 中写 return 语句。这是 Java 代码规范的铁律。finally 中的 return 会静默丢弃 try 中的异常,是极其隐蔽的 bug 来源。

面试官:"try 中 return,catch 中也 return,finally 还执行吗?"

public int test() {
    try {
        return 1;
    } catch (Exception e) {
        return 2;
    } finally {
        System.out.println("finally"); // 仍然执行!
    }
}

// finally 先执行,然后根据是否有异常/哪个分支,返回对应值

finally 总是在 return 之前执行,不管 return 在 try 还是 catch 中。

【面试官心理】 能说出"finally 中 return 丢弃异常"的候选人,是真正踩过坑或者读过 Effective Java 的。Effective Java 明确反对这种写法,这是 P6+ 的加分点。

五、生产避坑

5.1 常见错误:用 finally 做资源清理

// ❌ 错误写法:finally 中的异常会覆盖 try 中的异常
try {
    doSomething();
} finally {
    cleanup(); // 如果 cleanup 抛异常,doSomething 的异常就丢失了
}

// ✅ 正确写法:嵌套 try
try {
    try {
        doSomething();
    } finally {
        cleanupQuietly(); // 只记录,不抛异常
    }
} catch (Exception e) {
    handleError(e);
}

5.2 用 try-with-resources 替代手动 finally

// ❌ 传统写法:finally 容易出错
Connection conn = null;
try {
    conn = getConnection();
    conn.execute();
} finally {
    if (conn != null) conn.close(); // 容易忘记 null 检查
}

// ✅ 正确写法:try-with-resources
try (Connection conn = getConnection()) {
    conn.execute();
} // 自动 close,且异常不会互相覆盖