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.timeAPI。
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 知识问答
- try-catch 的基本作用是什么?
- try 中没有异常时 catch 是否执行?
- try 中发生异常后,后续代码是否继续执行?
- catch 参数
e本质是什么? getMessage()有什么作用?printStackTrace()有什么作用?- 为什么
catch(Exception e)能捕获 ArithmeticException? - 多个 catch 如何决定执行哪一个?
- 为什么子类 catch 应该写在父类 catch 前面?
- 同级异常的 catch 是否存在固定先后顺序?
- 什么叫吞异常?
- 为什么空 catch 是危险写法?
- finally 的作用是什么?
- 没有异常时 finally 是否执行?
- catch 执行后 finally 是否执行?
- 为什么不能绝对说 finally 百分之百执行?
- 为什么不建议 finally 中写 return?
- try-with-resources 与 finally 有什么关系?
- 为什么 try 的范围不应该无限扩大?
- 为什么异常处理的目标不是“让控制台没有红字”?
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");
}
}
不运行程序回答:
- A、B、C、D、E 哪些会输出?
- 为什么 C 不执行?
- catch 执行完后程序从哪里继续?
- 异常处理是否会重新执行
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");
}
}
回答:
- 输出顺序是什么?
- finally 为什么能够执行?
- 如果
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(
"数字格式错误"
);
}
}
}
要求:
- 判断程序能否正常编译。
- 找出问题发生的原因。
- 画出相关异常继承关系。
- 调整 catch 顺序。
- 思考是否真的需要保留
catch(Exception e)。
继续 Debug:
try {
doSomething();
} catch (Exception e) {
}
回答:
- 这段代码在语法上是否一定错误?
- 为什么工程上非常危险?
- 至少提出三种比空 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。
- [ ] 能独立完成一个包含异常恢复逻辑的小程序。