捕获异常 try-catch
本教程共 100 篇 · 第 66 篇 · 更新于 2026-08-05 · 约 16 分钟阅读
66. 捕获异常 try-catch
本节目标:掌握 try-catch 的完整语法和执行流程,学完能写出顺序正确、分支合理的多 catch 结构,避开吞异常这个大坑。
最小可用形态
try...catch 的基本结构只有两块:可能出错的代码放 try 里,出错后的补救逻辑放 catch 里。
public class BasicTryCatch {
public static void main(String[] args) {
try {
int result = 10 / 0; // 抛出 ArithmeticException
System.out.println("结果是 " + result);
} catch (ArithmeticException e) {
System.out.println("除数不能为零:" + e.getMessage());
}
System.out.println("程序继续往下跑");
}
}
```text
运行结果:
```text
除数不能为零:/ by zero
程序继续往下跑
注意两个细节。
try 里 10 / 0 之后的那行打印没有执行——异常一抛出,try 块立刻中断,剩下的语句全部跳过。
而 catch 之后的代码照常执行。异常被接住了,程序就从「崩溃」状态恢复成了「正常」状态。
执行流程拆解
把流程拆成两种情况看,就不容易绕晕。
没出异常:try 块从头跑到尾,所有 catch 分支被完全跳过。
出了异常:try 块在出错那一行中断,JVM 拿着这个异常对象,从上到下逐个比对 catch 声明的类型。匹配上了就执行那个 catch 块,然后跳出整个结构继续往下。
如果一个 catch 都没匹配上呢?异常继续往调用它的上一层方法传,一路传到 main 还没人接,程序就终止了。
catch 的匹配规则
catch (SomeException e) 的匹配不要求类型完全相等,子类也算匹配。
public class CatchParent {
public static void main(String[] args) {
try {
Integer.parseInt("abc"); // 抛 NumberFormatException
} catch (IllegalArgumentException e) {
// NumberFormatException 是 IllegalArgumentException 的子类,能接住
System.out.println("接住了:" + e.getClass().getSimpleName());
}
}
}
```bash
输出是 `接住了:NumberFormatException`。
变量 `e` 的**声明类型**是 `IllegalArgumentException`,但它指向的**实际对象**是 `NumberFormatException`。这就是多态在异常处理里的体现。
正因为父类能接子类,才有了下面这条容易踩的坑。
## 多个 catch:子类必须写在前面
一个 `try` 后面可以跟多个 `catch`,每个负责一种错误。
```java
import java.io.IOException;
public class MultiCatch {
public static void main(String[] args) {
try {
doWork();
} catch (NumberFormatException e) {
System.out.println("数字格式错误");
} catch (IOException e) {
System.out.println("IO 错误");
} catch (Exception e) {
System.out.println("其他错误");
}
}
static void doWork() throws IOException {
Integer.parseInt("x");
}
}
多个 catch 里最多只有一个会执行。JVM 从上往下找,找到第一个能匹配的就停手,后面的连看都不看。
这就带来一个硬性要求:范围小的(子类)必须写在范围大的(父类)前面。
// 错误示范:编译不通过
try {
doWork();
} catch (Exception e) { // 父类在前,把路堵死了
System.out.println("其他错误");
} catch (IOException e) { // 永远轮不到它
System.out.println("IO 错误");
}
```java
`IOException` 是 `Exception` 的子类。既然 `Exception` 排在前面,任何异常都会被它先接走,后面那个 `catch` 成了永远执行不到的死代码。
> [!WARNING]
> 这种顺序错误 Java 编译器会直接报错:`exception IOException has already been caught`。不是运行时才发现,是编译期就拦住你。
调整成子类在前就对了:
```java
try {
doWork();
} catch (IOException e) { // 具体的先来
System.out.println("IO 错误");
} catch (Exception e) { // 兜底的放最后
System.out.println("其他错误");
}
记法很简单:从具体到笼统,从小到大。
多重捕获:一个 catch 管多种异常
有时候几种异常的处理方式一模一样,但它们之间没有继承关系,没法用父类合并。
// 啰嗦写法:两段一样的代码
try {
doWork();
} catch (IOException e) {
System.out.println("输入有问题");
} catch (NumberFormatException e) {
System.out.println("输入有问题");
}
```java
Java 7 起支持**多重捕获**,用 `|` 把多个异常类型串起来:
```java
import java.io.IOException;
public class UnionCatch {
public static void main(String[] args) {
try {
doWork();
} catch (IOException | NumberFormatException e) {
// 两种异常走同一段处理逻辑
System.out.println("输入有问题:" + e.getMessage());
}
}
static void doWork() throws IOException {
Integer.parseInt("bad");
}
}
用 | 有两条限制要记住。
类型之间不能有继承关系。写 catch (IOException | FileNotFoundException e) 会编译报错,因为后者是前者的子类,写它纯属多余。
变量 e 是隐式 final 的,在 catch 块里不能给它重新赋值。
捕获后能拿到什么
catch 抓到的 e 是一个完整对象,身上带着排查问题需要的全部线索。
public class ExceptionInfo {
public static void main(String[] args) {
try {
Object obj = "我是字符串";
Integer num = (Integer) obj; // ClassCastException
System.out.println(num);
} catch (ClassCastException e) {
System.out.println("类型名:" + e.getClass().getName());
System.out.println("消息:" + e.getMessage());
System.out.println("---- 完整调用栈 ----");
e.printStackTrace();
}
}
}
```bash
`getMessage()` 拿到人话描述,`getClass().getName()` 拿到异常类全名,`printStackTrace()` 打印从 main 方法到出错点的完整路径。
调试阶段 `printStackTrace()` 最好用。但**正式代码里应该换成日志框架**,原因在第 71 章会细说。
## 变量作用域的坑
`try` 块里声明的变量,**出了 `try` 就不认识了**。
```java
// 编译不通过
try {
int n = Integer.parseInt("100");
} catch (NumberFormatException e) {
System.out.println("解析失败");
}
System.out.println(n); // 报错:找不到符号 n
try { } 是一个独立的代码块,大括号一闭合,里面的局部变量就销毁了。
需要在 try 外面用的变量,得提到 try 前面声明:
public class ScopeDemo {
public static void main(String[] args) {
int n = 0; // 提前声明并给默认值
try {
n = Integer.parseInt("100");
} catch (NumberFormatException e) {
System.out.println("解析失败,用默认值 0");
}
System.out.println("n = " + n); // 现在能访问了
}
}
```bash
## 三种典型错误写法
**第一种:空 catch 块。**
```java
// 千万别这么写
try {
riskyOperation();
} catch (Exception e) {
// 什么都不干
}
这叫「吞异常」。程序出了错,你既不处理也不记录,等于把警报器拆了。日后线上出问题,日志里干干净净,排查起来能让人抓狂。
哪怕真的没法处理,至少要留个痕迹:
try {
riskyOperation();
} catch (Exception e) {
e.printStackTrace(); // 先记下来再说
}
```bash
**第二种:catch 范围过大。**
```java
// 不推荐
try {
parseConfig();
} catch (Exception e) {
System.out.println("配置解析失败");
}
catch (Exception e) 会把 NullPointerException 这种代码 bug 一起接走。本该暴露出来立刻修的问题,被伪装成了「配置解析失败」。
捕获你真正预期会发生、也真正知道该怎么办的异常,其他的让它往上抛。
第三种:用 catch 代替判断。
// 不推荐:拿异常当流程控制
try {
System.out.println(arr[i]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("越界了");
}
```bash
越界完全可以用 `if (i >= 0 && i < arr.length)` 提前判断。异常机制的创建和栈捕获都有开销,**能用条件判断解决的,就别用异常**。
> [!TIP]
> 一个判断标准:如果这个「异常」在正常业务流程里会频繁发生,那它就不该是异常,该用 `if` 处理。
## try 嵌套:能不用就不用
`try` 结构可以嵌套,内层没接住的异常会交给外层继续匹配。
```java
public class NestedTry {
public static void main(String[] args) {
try {
try {
Object obj = "字符串";
Integer n = (Integer) obj; // ClassCastException
} catch (NumberFormatException e) {
// 类型对不上,接不住
System.out.println("内层:数字格式错误");
}
} catch (ClassCastException e) {
System.out.println("外层接住了:" + e.getMessage());
}
}
}
输出是「外层接住了:…」。内层的 catch 类型不匹配,异常就穿透出去交给外层了。
语法上没问题,但嵌套超过两层的代码非常难读。真遇到这种情况,通常说明方法太长了,把内层逻辑抽成独立方法会清爽得多。
// 把内层抽成方法,结构立刻变平
public class FlatVersion {
public static void main(String[] args) {
try {
doInnerWork();
} catch (ClassCastException e) {
System.out.println("处理:" + e.getMessage());
}
}
static void doInnerWork() {
Object obj = "字符串";
Integer n = (Integer) obj;
System.out.println(n);
}
}
```bash
## catch 里能做什么
抓到异常之后,常见的处理方式有四种,按使用频率排:
**记录后继续。** 这个错不影响主流程,记一笔接着跑。
```java
try {
sendNotification(user);
} catch (Exception e) {
// 通知发失败不该影响下单主流程
System.out.println("通知发送失败,已记录:" + e.getMessage());
}
用默认值兜底。 拿不到真实值就退而求其次。
int timeout;
try {
timeout = Integer.parseInt(config.get("timeout"));
} catch (NumberFormatException e) {
timeout = 3000; // 配置有问题,用默认超时
}
```java
**转换成另一种异常再抛。** 把底层技术异常包装成业务语义,第 70 章会详细展开。
**重试。** 网络抖动这类临时故障,隔一会儿再试一次可能就好了。
```java
public class RetryDemo {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
try {
callRemote();
System.out.println("第 " + i + " 次调用成功");
break;
} catch (RuntimeException e) {
System.out.println("第 " + i + " 次失败:" + e.getMessage());
}
}
}
static void callRemote() {
if (Math.random() < 0.7) {
throw new RuntimeException("网络超时");
}
}
}
选哪种,取决于这个错误对业务的影响有多大。无关紧要的记一笔就行,核心流程失败就得往上抛让调用方知道。
小结
try 装可能出错的代码,catch 装出错后的补救逻辑,异常一抛 try 块立刻中断。
catch 能接住声明类型及其所有子类,所以多个 catch 必须子类在前、父类在后,顺序写反编译器直接报错。
处理逻辑相同又无继承关系的异常,用 catch (A | B e) 合并,注意变量 e 是隐式 final 的。
三条红线:不写空 catch、不无脑 catch (Exception e)、不拿异常当 if 用。