try-catch-finally 异常捕获 | JavaSE

try-catch-finally 异常捕获

一、学习目标

完成本章后,你应该能够:

  • 能够独立写出 try-catch 的基本语法。
  • 能够解释异常发生后 try 代码块的执行中断过程。
  • 能够根据异常继承关系选择合理的 catch 类型。
  • 能够正确设计多个 catch 的顺序。
  • 能够理解 catch(Exception e) 的捕获范围及其利弊。
  • 能够使用 getMessage()printStackTrace() 获取异常信息。
  • 能够解释 finally 的执行时机和适用场景。
  • 能够识别“吞异常”、空 catch、过度捕获等错误处理方式。
  • 能够根据场景决定异常发生后是提示、恢复、记录还是终止当前操作。

二、核心知识

2.1 什么叫“捕获异常”

程序执行过程中发生异常后,如果没有任何代码处理:

发生异常
    ↓
当前代码无法继续正常执行
    ↓
异常沿调用链继续传播
    ↓
如果始终没人处理
    ↓
当前线程最终异常终止

Java 提供:

try-catch

建立一个异常处理器。

基本思想:

try
    ↓
执行可能失败的代码
    ↓
如果发生指定异常
    ↓
跳转到对应 catch
    ↓
执行异常处理逻辑

2.2 try-catch 基本语法

try {
    // 可能出现异常的代码
} catch (异常类型 e) {
    // 异常处理代码
}

例如:

public class TryCatchDemo {
    public static void main(String[] args) {
        try {
            int result = 10 / 0;
            System.out.println(result);
        } catch (ArithmeticException e) {
            System.out.println("除数不能为 0");
        }

        System.out.println("程序继续执行");
    }
}

其中:

try {
    ...
}

负责包围:

可能出现异常的代码

而:

catch (ArithmeticException e) {
    ...
}

表示:

如果发生的异常对象能够被 ArithmeticException 类型接收,就由这个 catch 处理。


2.3 try 中没有异常时

例如:

try {
    int result = 10 / 2;
    System.out.println(result);
} catch (ArithmeticException e) {
    System.out.println("发生算术异常");
}

System.out.println("end");

执行流程:

进入 try
    ↓
10 / 2 正常
    ↓
输出结果
    ↓
没有异常
    ↓
跳过 catch
    ↓
继续执行 end

所以:

catch 不是每次都会执行。

只有发生了它能够处理的异常时,才进入对应 catch


2.4 try 中发生异常时

例如:

try {
    System.out.println("A");

    int result = 10 / 0;

    System.out.println("B");
} catch (ArithmeticException e) {
    System.out.println("C");
}

System.out.println("D");

关键点是:

异常发生后
try 中当前执行路径会立即中断

所以:

System.out.println("B");

不会继续执行。

执行流程:

输出 A
    ↓
10 / 0
    ↓
ArithmeticException
    ↓
try 剩余代码终止
    ↓
寻找匹配 catch
    ↓
输出 C
    ↓
catch 正常结束
    ↓
输出 D

这说明:

catch 处理完异常后,并不是回到异常发生的那一行重新继续执行。

它会从整个 try-catch 结构之后继续。


2.5 catch 的异常参数是什么

例如:

catch (ArithmeticException e) {
    ...
}

这里:

e

就是:

被捕获到的异常对象引用

本质上仍然属于:

对象

因此可以调用:

e.getMessage();
e.printStackTrace();

等方法。


2.6 getMessage()

例如:

try {
    int result = 10 / 0;
    System.out.println(result);
} catch (ArithmeticException e) {
    System.out.println(e.getMessage());
}

getMessage() 用于获得:

异常对象中的详细消息

它通常比较简短。

适合:

记录异常原因
调试
构建日志信息

但不能保证所有异常都有非常适合直接展示给最终用户的 message。


2.7 printStackTrace()

原教学资料重点使用:

e.printStackTrace();

例如:

try {
    int result = 10 / 0;
    System.out.println(result);
} catch (ArithmeticException e) {
    e.printStackTrace();
}

它会输出:

异常类型
异常消息
调用堆栈
代码位置

对于学习和 Debug 非常重要。

例如可能看到:

java.lang.ArithmeticException: / by zero
    at ...

这能够帮助定位:

异常发生在哪个类
哪个方法
哪一行

2.8 捕获父类异常

因为异常体系本质仍然是继承体系:

ArithmeticException
        ↓
RuntimeException
        ↓
Exception
        ↓
Throwable

所以:

catch (Exception e)

也能够捕获:

ArithmeticException

例如:

try {
    int result = 10 / 0;
} catch (Exception e) {
    e.printStackTrace();
}

这利用的是:

多态

可以理解为:

Exception e =
        new ArithmeticException();

父类引用可以接收子类对象。


2.9 catch(Exception e) 的优点和问题

原课程为了帮助初学者快速建立异常处理模型,经常使用:

catch (Exception e)

它确实非常方便。

因为大量普通异常都属于:

Exception

体系。

但工程开发中不能因此形成:

所有地方都 catch(Exception)

的习惯。

例如:

try {
    ...
} catch (Exception e) {
    System.out.println("出错了");
}

可能把:

文件不存在
数字格式错误
SQL 异常
业务异常
空指针

全部压缩成一句:

出错了

异常类型携带的信息被彻底丢失。

因此:

能捕获得更具体时,通常优先考虑更具体的异常类型。


2.10 多个 catch

一个 try 后面可以存在多个 catch

例如:

try {
    String text = "abc";
    int number = Integer.parseInt(text);

    System.out.println(10 / number);
} catch (NumberFormatException e) {
    System.out.println("数字格式不正确");
} catch (ArithmeticException e) {
    System.out.println("发生算术异常");
}

程序发生异常后,Java 会寻找:

第一个能够处理这个异常的 catch

然后执行。


2.11 多 catch 为什么要“子类在前,父类在后”

假设:

try {
    ...
} catch (Exception e) {
    ...
} catch (NumberFormatException e) {
    ...
}

这是错误的。

原因:

NumberFormatException
    ↓
IllegalArgumentException
    ↓
RuntimeException
    ↓
Exception

前面的:

catch (Exception e)

已经可以捕获:

NumberFormatException

那么后面的:

catch (NumberFormatException e)

永远没有机会执行。

因此正确顺序应该是:

try {
    ...
} catch (NumberFormatException e) {
    ...
} catch (Exception e) {
    ...
}

原则:

具体异常
    ↓
一般异常

子类
    ↓
父类

2.12 同级异常之间没有父子顺序限制

例如:

ArithmeticException
NumberFormatException

它们都属于 RuntimeException 体系,但彼此没有直接父子关系。

因此:

catch (ArithmeticException e) {
    ...
} catch (NumberFormatException e) {
    ...
}

或者反过来都可以。

真正需要关注的是:

是否存在父子继承关系

2.13 多异常联合捕获

Java 还支持:

catch (异常A | 异常B e)

例如:

try {
    ...
} catch (NumberFormatException |
         ArithmeticException e) {
    System.out.println("输入或计算失败");
}

适合:

多个不同异常
采用完全相同处理策略

的情况。

但是不要为了少写几个 catch,把语义完全不同的问题强行合并。


三、使用方法

3.1 捕获一个 RuntimeException

public class NumberDemo {
    public static void main(String[] args) {
        String input = "abc";

        try {
            int number =
                    Integer.parseInt(input);

            System.out.println(number);
        } catch (NumberFormatException e) {
            System.out.println(
                    "请输入合法整数"
            );
        }

        System.out.println("程序结束");
    }
}

这里异常处理的意义是:

用户输入非法
    ↓
转换失败
    ↓
捕获 NumberFormatException
    ↓
给用户清晰提示
    ↓
程序可以继续执行

3.2 捕获 Checked Exception

例如使用源课程中的日期解析案例:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateParseDemo {
    public static void main(String[] args) {
        SimpleDateFormat sdf =
                new SimpleDateFormat(
                        "yyyy/MM/dd HH:mm:ss"
                );

        try {
            Date date =
                    sdf.parse(
                            "2026/09/02 19:30:00"
                    );

            System.out.println(date);
        } catch (ParseException e) {
            System.out.println(
                    "日期格式解析失败"
            );
        }
    }
}

这里:

ParseException

属于 Checked Exception。

通过:

catch (ParseException e)

进行捕获后,编译器的异常处理要求得到满足。

这里使用 SimpleDateFormat 只是为了与本课程异常源案例保持一致,并演示 Checked Exception。现代 Java 日期时间业务优先学习和使用此前已经介绍过的 java.time API。


3.3 try 的范围不要无限扩大

不推荐:

try {
    // 几百行代码
}

然后:

catch (Exception e) {
    ...
}

因为当异常发生时,很难直接判断:

到底是哪一个操作失败

合理的 try 范围应该:

围绕一个明确的失败边界

例如:

解析用户输入
读取文件
访问数据库
调用远程服务

而不是把整个程序全部塞进去。


3.4 不要写空 catch

危险代码:

try {
    int number =
            Integer.parseInt(input);
} catch (NumberFormatException e) {
}

程序发生异常后:

什么信息都没有
什么恢复动作都没有
什么日志都没有

这种行为称为:

吞异常

它会导致:

程序明明失败
但外部看起来似乎成功

后期 Debug 会非常困难。


3.5 catch 至少应该做什么

根据场景,catch 中通常应该执行一种或多种动作:

记录异常
给用户合理提示
转换异常
执行恢复逻辑
终止当前操作
返回失败结果

而不是统一:

System.out.println("有异常");

四、原理与进阶

4.1 catch 的匹配本质

假设实际产生:

NumberFormatException

Java 会按照 catch 出现顺序寻找。

概念上类似判断:

异常对象 instanceof catch声明的类型

第一个匹配成功的:

catch

获得控制权。

所以:

多个 catch

不是全部执行。

而是:

第一个能接住这个异常的 catch 执行。


4.2 异常发生后为什么不会继续执行 try 剩余代码

异常发生意味着当前语句:

Abrupt Completion

即执行发生非正常完成。

Java 会从异常发生点开始寻找异常处理器。

所以:

try {
    statementA();
    statementB(); // 这里抛异常
    statementC();
}

如果:

statementB()

抛出了异常,

那么:

statementC()

不会执行。

执行流已经离开当前正常路径。


4.3 什么是 finally

语法:

try {
    ...
} catch (Exception e) {
    ...
} finally {
    ...
}

finally 表示:

当控制流离开 try / catch 结构时,用于执行收尾逻辑的代码块。

例如:

public class FinallyDemo {
    public static void main(String[] args) {
        try {
            System.out.println("try");

            int number = 10 / 0;

            System.out.println(number);
        } catch (ArithmeticException e) {
            System.out.println("catch");
        } finally {
            System.out.println("finally");
        }

        System.out.println("end");
    }
}

执行结果:

try
catch
finally
end

4.4 没有异常时 finally 会不会执行

会。

例如:

try {
    System.out.println("正常执行");
} finally {
    System.out.println("finally");
}

即使:

try 正常结束

仍然进入:

finally

4.5 有异常时 finally 会不会执行

通常也会。

try
    ↓
异常
    ↓
catch
    ↓
finally

即使异常没有被当前 catch 正确处理,控制流离开当前 try 结构之前,通常也会执行 finally。

因此 finally 非常适合:

收尾操作
资源释放
状态清理

4.6 finally 真的是“绝对百分百执行”吗

教学阶段经常说:

finally 一定执行。

它表达的是正常 Java 控制流中的核心规律,但严格来说存在例外。

例如:

JVM / 进程被强制终止
机器崩溃
代码永远无法离开 try

都可能导致 finally 没有机会完成。

因此更加严谨的说法是:

当 Java 控制流正常或异常地离开对应 try/catch 时,finally 通常都会执行。


4.7 finally 与 return

例如:

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

即使 try 中准备:

return 10

finally 通常仍会先执行。

因此不要在 finally 中随意:

return

例如:

public static int test() {
    try {
        return 10;
    } finally {
        return 20;
    }
}

这种代码会严重干扰正常控制流,并可能覆盖原来的返回行为。

工程中应尽量避免。


4.8 finally 中也不要随意抛出新的异常

如果:

try 中已经出现异常 A

finally 又产生:

异常 B

可能导致原始异常信息被新的异常控制流覆盖。

所以 finally 应该尽量:

简单
可靠
专注收尾

不要塞入大量容易失败的复杂业务。


五、实践应用

5.1 用户输入转换失败

try {
    int age =
            Integer.parseInt(input);
} catch (NumberFormatException e) {
    System.out.println(
            "年龄必须输入整数"
    );
}

这里捕获异常是有意义的。

因为:

输入非法

属于用户可能发生的正常错误。

程序可以:

提示用户重新输入

实现恢复。


5.2 最外层统一处理

原课程提出一种非常重要的工程思想:

底层发生问题
    ↓
逐层通知上层
    ↓
最外层统一处理

例如:

数据访问层
    ↓
业务层
    ↓
Controller / UI

最外层通常更清楚应该:

记录日志
返回什么状态码
显示什么用户提示
是否重试

不过如何让异常真正沿方法调用链传播,属于下一章 throw / throws 的内容。


5.3 finally 与资源释放

传统 Java 代码中经常看到:

打开资源
    ↓
try
    ↓
使用资源
    ↓
finally
    ↓
关闭资源

例如文件、网络连接等。

但现代 Java 对实现:

AutoCloseable

的资源提供:

try-with-resources

自动资源管理机制。

完整的资源关闭问题将在 File 与 IO 章节系统学习。

本章只需要理解:

finally 可以承担收尾职责,但现代资源管理很多时候应该优先考虑 try-with-resources。


六、常见问题

6.1 try 中所有代码都会执行吗?

不一定。

异常发生之后,try 中当前执行路径后面的语句会被跳过。


6.2 catch 后程序一定继续执行吗?

如果:

catch 自身正常结束

并且没有新的异常或其他终止行为,程序通常从整个 try-catch 结构之后继续。

但不会回到原异常语句继续运行。


6.3 一个异常会执行多个 catch 吗?

不会。

Java 会选择:

第一个能够处理当前异常的 catch

执行。


6.4 为什么父类 catch 不能放在子类 catch 前面?

因为父类 catch 已经能够捕获子类异常。

后面的子类 catch 永远无法到达,因此编译器会拒绝这种结构。


6.5 catch(Exception e) 能不能用?

可以。

但是捕获范围非常大。

教学阶段可以用来理解机制,真实项目应考虑:

是否可以使用更具体的异常类型

6.6 printStackTrace() 是不是生产环境日志方案?

它非常适合:

学习
本地调试
观察异常堆栈

正式项目通常应该使用统一日志框架和异常处理机制,而不是到处直接:

e.printStackTrace();

6.7 什么是吞异常?

例如:

catch (Exception e) {
}

异常被捕获后:

不记录
不处理
不恢复
不传播

导致失败信息消失。

这是非常危险的异常处理方式。


6.8 finally 一定执行吗?

正常 Java 控制流离开 try/catch 时通常执行。

但是:

JVM 被终止
进程崩溃
程序永远停留在 try 中

等情况下不能简单宣称它百分之百执行。


6.9 finally 中应该写 return 吗?

通常不要。

它会使控制流变得难以理解,并可能覆盖 try/catch 原有的返回或异常行为。


6.10 finally 是否专门用于关闭文件?

不是。

finally 本质是:

收尾代码块

资源释放只是它最经典的应用之一。


七、练习与验收

7.1 知识问答

  1. try-catch 的基本作用是什么?
  2. try 中没有异常时 catch 是否执行?
  3. try 中发生异常后,后续代码是否继续执行?
  4. catch 参数 e 本质是什么?
  5. getMessage() 有什么作用?
  6. printStackTrace() 有什么作用?
  7. 为什么 catch(Exception e) 能捕获 ArithmeticException?
  8. 多个 catch 如何决定执行哪一个?
  9. 为什么子类 catch 应该写在父类 catch 前面?
  10. 同级异常的 catch 是否存在固定先后顺序?
  11. 什么叫吞异常?
  12. 为什么空 catch 是危险写法?
  13. finally 的作用是什么?
  14. 没有异常时 finally 是否执行?
  15. catch 执行后 finally 是否执行?
  16. 为什么不能绝对说 finally 百分之百执行?
  17. 为什么不建议 finally 中写 return?
  18. try-with-resources 与 finally 有什么关系?
  19. 为什么 try 的范围不应该无限扩大?
  20. 为什么异常处理的目标不是“让控制台没有红字”?

7.2 代码阅读

阅读:

public class TryCatchRead01 {
    public static void main(String[] args) {
        System.out.println("A");

        try {
            System.out.println("B");

            int value = 10 / 0;

            System.out.println("C");
        } catch (ArithmeticException e) {
            System.out.println("D");
        }

        System.out.println("E");
    }
}

不运行程序回答:

  1. A、B、C、D、E 哪些会输出?
  2. 为什么 C 不执行?
  3. catch 执行完后程序从哪里继续?
  4. 异常处理是否会重新执行 10 / 0

阅读:

public class FinallyRead01 {
    public static void main(String[] args) {
        try {
            System.out.println("1");
            int value = 10 / 0;
            System.out.println(value);
        } catch (ArithmeticException e) {
            System.out.println("2");
        } finally {
            System.out.println("3");
        }

        System.out.println("4");
    }
}

回答:

  1. 输出顺序是什么?
  2. finally 为什么能够执行?
  3. 如果 10 / 0 改成 10 / 2,哪些输出会发生变化?

7.3 手写代码

任务一:整数输入

读取一个字符串并尝试转换为整数。

要求:

输入合法
→ 输出转换后的整数

输入非法
→ 提示“请输入合法整数”

只捕获最合适的异常类型。


任务二:多 catch

设计一个程序,使代码可能产生:

NumberFormatException
ArithmeticException

分别使用两个 catch 处理。

要求:

  • 每种异常输出不同提示。
  • 不允许直接只写一个 catch(Exception e)

任务三:finally

编写一个程序:

try 输出 start
产生异常
catch 输出 failed
finally 输出 cleanup
try-catch-finally 后输出 end

先预测完整输出,再运行验证。

7.4 Debug

下面代码存在问题:

public class CatchDebug {
    public static void main(String[] args) {
        try {
            int number =
                    Integer.parseInt("Java");

            System.out.println(10 / number);
        } catch (Exception e) {
            System.out.println("发生错误");
        } catch (NumberFormatException e) {
            System.out.println(
                    "数字格式错误"
            );
        }
    }
}

要求:

  1. 判断程序能否正常编译。
  2. 找出问题发生的原因。
  3. 画出相关异常继承关系。
  4. 调整 catch 顺序。
  5. 思考是否真的需要保留 catch(Exception e)

继续 Debug:

try {
    doSomething();
} catch (Exception e) {
}

回答:

  1. 这段代码在语法上是否一定错误?
  2. 为什么工程上非常危险?
  3. 至少提出三种比空 catch 更合理的处理方案。

7.5 综合训练

实现一个控制台整数计算器。

用户依次输入:

第一个整数
运算符
第二个整数

支持:

+
-
*
/

要求处理:

数字格式错误
除数为 0
非法运算符

并设计:

try
多个 catch
finally

要求:

  • 不允许一个巨大的 catch(Exception e) 解决所有问题。
  • 不允许空 catch。
  • 对不同失败原因给出不同提示。
  • finally 中输出一次“本次计算结束”。
  • 分析哪些情况真正应该使用异常,哪些更适合普通 if 判断。

7.6 本章验收

达到下面标准,可以认为本章基本掌握:

  • [ ] 能闭卷写出 try-catch。
  • [ ] 能准确描述 try-catch 的执行流程。
  • [ ] 能解释异常发生后 try 剩余代码为什么不执行。
  • [ ] 能根据异常继承关系选择 catch 类型。
  • [ ] 能独立写多个 catch。
  • [ ] 能解释子类 catch 为什么必须位于父类 catch 前。
  • [ ] 能使用 getMessage() 与 printStackTrace()。
  • [ ] 能识别并拒绝空 catch。
  • [ ] 能解释 finally 的作用和执行时机。
  • [ ] 能解释为什么 finally 中不推荐 return。
  • [ ] 能独立完成一个包含异常恢复逻辑的小程序。