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

Java 入门教程

捕获异常 try-catch

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

JavaJava 入门教程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("程序继续往下跑");
    }
}

运行结果:

除数不能为零:/ 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());
        }
    }
}

输出是 接住了:NumberFormatException。

变量 e 的声明类型是 IllegalArgumentException,但它指向的实际对象是 NumberFormatException。这就是多态在异常处理里的体现。

正因为父类能接子类,才有了下面这条容易踩的坑。

多个 catch:子类必须写在前面

一个 try 后面可以跟多个 catch,每个负责一种错误。

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 错误");
}

IOException 是 Exception 的子类。既然 Exception 排在前面,任何异常都会被它先接走,后面那个 catch 成了永远执行不到的死代码。

Warning

这种顺序错误 Java 编译器会直接报错:exception IOException has already been caught。不是运行时才发现,是编译期就拦住你。

调整成子类在前就对了:

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 7 起支持多重捕获,用 | 把多个异常类型串起来:

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();
        }
    }
}

getMessage() 拿到人话描述,getClass().getName() 拿到异常类全名,printStackTrace() 打印从 main 方法到出错点的完整路径。

调试阶段 printStackTrace() 最好用。但正式代码里应该换成日志框架,原因在第 71 章会细说。

变量作用域的坑

try 块里声明的变量,出了 try 就不认识了。

// 编译不通过
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); // 现在能访问了
    }
}

三种典型错误写法

第一种:空 catch 块。

// 千万别这么写
try {
    riskyOperation();
} catch (Exception e) {
    // 什么都不干
}

这叫「吞异常」。程序出了错,你既不处理也不记录,等于把警报器拆了。日后线上出问题,日志里干干净净,排查起来能让人抓狂。

哪怕真的没法处理,至少要留个痕迹:

try {
    riskyOperation();
} catch (Exception e) {
    e.printStackTrace(); // 先记下来再说
}

第二种:catch 范围过大。

// 不推荐
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("越界了");
}

越界完全可以用 if (i >= 0 && i < arr.length) 提前判断。异常机制的创建和栈捕获都有开销,能用条件判断解决的,就别用异常。

Tip

一个判断标准:如果这个「异常」在正常业务流程里会频繁发生,那它就不该是异常,该用 if 处理。

try 嵌套:能不用就不用

try 结构可以嵌套,内层没接住的异常会交给外层继续匹配。

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);
    }
}

catch 里能做什么

抓到异常之后,常见的处理方式有四种,按使用频率排:

记录后继续。 这个错不影响主流程,记一笔接着跑。

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; // 配置有问题,用默认超时
}

转换成另一种异常再抛。 把底层技术异常包装成业务语义,第 70 章会详细展开。

重试。 网络抖动这类临时故障,隔一会儿再试一次可能就好了。

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