← 返回题目列表

try-catch-finally 的执行顺序是怎样的?finally 一定会执行吗?

高频 中等 第 18 / 32 题 更新于 2026/07/26
异常finallytry-catch执行顺序

简化版

正常顺序:try → (有异常)catchfinallyfinally 几乎总会执行——无论 try 里是正常结束、抛异常、还是有 return,finally 都会在方法真正返回前执行。唯一不执行的情况是:System.exit() 直接终止 JVM、或线程被强行杀死/死循环/断电。最大的坑是 finally 里的 return 会覆盖 try/catch 里的 return,而且 finally 修改基本类型返回值不影响已确定的返回值(因为 return 值在执行 finally 前已被暂存)。所以永远不要在 finally 里写 return。

详细版

基本执行顺序

try {
    // 1. 先执行 try
    return a;           // 3. 计算返回值 a 并暂存,但先不返回
} catch (Exception e) {
    // 2. try 抛异常才执行 catch
} finally {
    // 4. 无论如何都执行 finally(在真正 return 前)
}
// 5. finally 执行完,才真正把暂存的返回值返回

finally 一定执行吗——绝大多数情况是,但有例外:

情况finally 是否执行
try 正常结束✅ 执行
try 抛异常(被 catch 或未被 catch)✅ 执行
try/catch 里有 return✅ 执行(在 return 前)
try/catch 里有 break/continue✅ 执行
System.exit(0)不执行(JVM 直接退出)
线程被 kill、断电、死循环❌ 不执行

两个经典陷阱

// 陷阱1:finally 的 return 覆盖 try 的 return
int f1() {
    try { return 1; }
    finally { return 2; }   // 实际返回 2!finally 的 return 吃掉了 try 的
}

// 陷阱2:finally 改基本类型不影响返回值
int f2() {
    int x = 1;
    try { return x; }        // 返回值 1 已暂存
    finally { x = 2; }       // 改的是局部变量 x,暂存的返回值不变
}                            // 实际返回 1,不是 2

⚠️ finally 里绝不要写 return 或抛异常——它会吞掉 try/catch 里的 return 和异常,让真正的异常「消失」,极难排查。这是异常处理的头号反模式。

完整版教学

一、标准执行流程:finally 在 return 之前

先建立正确的顺序模型。很多人以为「return 就直接返回了」,其实 return 分两步:先算返回值并暂存,执行 finally,再真正返回

try {
    可能抛异常的代码
    return 表达式;   ← ① 计算表达式的值,暂存到一个隐藏位置
                       ② 不立即返回,先去执行 finally
} catch (e) {
    异常处理
    return 表达式;   ← 同样先暂存,再执行 finally
} finally {
    收尾代码         ← ③ 一定执行(在真正返回前)
}
                     ← ④ finally 执行完,把①暂存的值真正返回

核心认知:finally 卡在「算好返回值」和「真正返回」之间执行。这个位置决定了后面所有陷阱——因为返回值在 finally 之前就已经暂存好了,finally 里再改「原变量」也改不动那个暂存值(除非 finally 自己 return 一个新值)。

二、为什么 finally 几乎总会执行

finally 的设计目的是「保证收尾代码一定运行」——释放锁、关闭连接、清理资源。所以 JVM 保证:不管 try 块是正常走完、抛异常、还是 return/break/continue 想跳出去,都要先执行完 finally 再离开

try 想通过 return 离开   → 先执行 finally 再返回
try 抛异常想离开        → 先执行 finally 再把异常抛出去
try 里 break 跳出循环   → 先执行 finally 再 break

正因为这个「离开前必经 finally」的保证,finally 成了资源清理的标准位置。但它不是绝对的——两类情况会跳过 finally:System.exit()(直接命令 JVM 退出,连收尾都不给);② JVM 非正常终止(被 kill -9、断电、或 try 里死循环永远不「离开」)。除此之外,finally 必执行。

三、陷阱一:finally 的 return 吞掉一切

finally 里写 return,会覆盖 try/catch 里的 return,也会吞掉正在抛出的异常:

int f() {
    try {
        return 1;          // 想返回 1
    } finally {
        return 2;          // finally 的 return 直接替换,实际返回 2
    }
}

int g() {
    try {
        throw new RuntimeException("重要错误");  // 想抛异常
    } finally {
        return 0;          // 异常被吞!方法正常返回 0,异常凭空消失
    }
}

g() 的问题极其危险:一个本该抛出的异常,被 finally 的 return 悄悄吃掉了,方法看起来「正常返回」,上层完全不知道出过错。线上排查时会看到「没有任何异常,但结果不对」的诡异现象。所以铁律是:finally 只做资源清理,绝不 return、绝不抛异常

四、陷阱二:finally 改基本类型不影响返回值

结合第一节的「返回值先暂存」,就能理解这个反直觉现象:

int f2() {
    int x = 1;
    try {
        return x;      // ① 把 x 的当前值 1 暂存为返回值
    } finally {
        x = 2;         // ② 改的是局部变量 x,暂存的返回值还是 1
    }
}                      // ③ 返回暂存值 1,不是 2

关键:return x 执行时,已经把 x 的值(1)复制到了返回值暂存区,finally 里再把 x 改成 2,改的是变量 x,动不了那个已经存好的「返回值副本」。所以返回 1。

引用类型不一样——如果返回的是对象,finally 里修改对象的字段会影响到:

StringBuilder f3() {
    StringBuilder sb = new StringBuilder("a");
    try {
        return sb;         // 暂存的是 sb 的引用(地址)
    } finally {
        sb.append("b");    // 通过引用改对象内容,返回的对象就是 "ab"
    }
}                          // 实际返回 "ab"

因为暂存的是「引用(地址)」,finally 里通过同一个引用改对象内容,返回的对象自然带上了改动。基本类型暂存的是值、改不动;引用类型暂存的是地址、能改对象——这个区别是 finally 陷阱的精髓。

五、try-with-resources:finally 的现代替代

用 finally 手动关资源又啰嗦又容易错(还可能被 finally 里的异常掩盖),Java 7 的 try-with-resources 是更好的方案:

// 传统 finally 关资源(繁琐,且 close 抛异常会掩盖 try 的异常)
InputStream in = null;
try {
    in = new FileInputStream("f");
    // use in
} finally {
    if (in != null) in.close();   // close 若抛异常会覆盖 try 的异常
}

// try-with-resources(自动关闭,异常不丢失)
try (InputStream in = new FileInputStream("f")) {
    // use in
}   // 自动调用 in.close(),无需 finally

它要求资源实现 AutoCloseable,编译器会自动生成关闭逻辑,且关闭发生在 try 块之后、catch 之前。更妙的是它解决了「异常掩盖」:如果 try 和 close 都抛异常,主异常保留、close 的异常作为「被抑制的异常(suppressed)」附在主异常上,不会丢失。所以现在关资源优先用 try-with-resources,而不是手写 finally。

六、执行顺序综合演练

把几种情况串起来,用带 return 的嵌套场景检验理解:

int demo() {
    try {
        System.out.println("A");
        return compute();     // 先算 compute()(打印 B),暂存返回值,再去 finally
    } catch (Exception e) {
        System.out.println("C");
        return -1;
    } finally {
        System.out.println("D");   // 一定在真正返回前打印
    }
}
int compute() { System.out.println("B"); return 10; }

// 无异常时输出顺序:A → B → D,返回 10
// (A 打印 → 算 compute 打印 B 并暂存 10 → 执行 finally 打印 D → 返回 10)

顺序要点:try 里的 return 表达式先求值(可能有副作用如打印 B)→ 暂存 → 执行 finally(打印 D)→ 真正返回。catch 不执行(无异常)。掌握「表达式先求值、finally 卡在返回前」这条主线,任何组合都能推出来。

记忆钩子:「顺序 try→catch→finally,return 值先暂存再执行 finally;finally 几乎必执行(除了 System.exit);finally 别 return(吞异常盖返回值)、改基本类型不影响返回值(值已暂存)、改引用对象则有效」

七、常见误区与追问

  • 误区:try 里 return 后 finally 就不执行了。 会执行——return 先暂存返回值,执行 finally,再真正返回;finally 卡在「暂存」和「返回」之间。
  • 误区:finally 一定会执行。 绝大多数会,但 System.exit() 直接退出 JVM、或断电/kill/死循环时不执行。
  • 误区:finally 里改了返回变量,返回值就变了。 基本类型返回值在 return 时已暂存副本,finally 改原变量不影响;但引用类型改对象内容会影响(暂存的是地址)。
  • 误区:finally 里 return 没问题。 大问题——会覆盖 try/catch 的 return,还会吞掉正在抛出的异常,让错误凭空消失,是头号反模式。
  • 追问:finally 里 return 和 try 里 return 都有,返回哪个? 返回 finally 的——finally 的 return 会覆盖 try/catch 暂存的返回值,且吞掉异常,所以绝不要这么写。
  • 追问:为什么 finally 改基本类型返回值无效,改引用有效? return 时暂存的是值的副本或引用的地址;基本类型改的是原变量动不了副本,引用类型通过地址改对象内容能生效。
  • 追问:try-with-resources 比 finally 好在哪? 自动关闭(免写 finally)、代码简洁、且保留主异常把 close 异常作为 suppressed 附加,不会像手写 finally 那样让 close 异常掩盖真正的异常。

八、加强记忆

try-catch-finally 的顺序是 try →(异常时)catch → finally,但关键机制是「return 先算好返回值并暂存,再执行 finally,最后才真正返回」——finally 卡在「暂存返回值」和「真正返回」之间,这个位置解释了所有陷阱。finally 几乎总会执行(正常结束、抛异常、return、break 都会先跑完 finally),唯一例外是 System.exit() 或 JVM 被强杀/断电/死循环。两大陷阱要背死:① finally 里 return 会覆盖 try/catch 的 return 并吞掉异常(让错误凭空消失,头号反模式,绝不要写);② finally 改基本类型返回值无效(值已暂存副本),但改引用类型的对象内容有效(暂存的是地址)。现代关资源优先用 try-with-resources(自动关闭、代码简洁、异常不被掩盖),替代手写 finally。一句话「返回值先暂存再走 finally,finally 除 System.exit 必执行,别在 finally 里 return,改基本类型不影响返回值、改引用对象才影响」。