首页 / Java 入门教程 / 捕获异常 try-catch

Java 入门教程

捕获异常 try-catch

本教程共 100 篇 · 第 66 篇 · 更新于 2026-08-05 · 约 16 分钟阅读

JavaJava 入门教程try-catch捕获异常多重捕获

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
程序继续往下跑

注意两个细节。

try10 / 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 用。