☕ 第五章 异常

5.3 异常处理及语法

📄 一句话概括

异常处理的全部语法就五个关键词:try / catch / finally 负责"接"(try 圈住风险代码,catch 写预案,finally 收尾);throw 负责"主动甩"(方法体里抛异常对象);throws 负责"声明会甩"(方法签名上告知调用者)。学完本节,5.2 那个 Unhandled exception 报错就有了两条活路。

1. try-catch:自己换备胎

先把 5.1 炸掉的程序改造一遍——用 try 把风险代码圈住:

public class Demo03 {
    public static void main(String[] args) {
        try {
            int r = 10 / 0;                    // 风险代码
            System.out.println("算完了");       // 炸点之后,不执行
        } catch (ArithmeticException e) {
            // e 就是 5.1 说的那个"事故报告"对象
            System.out.println("接住了:" + e.getMessage());
        }
        System.out.println("程序结束");         // 正常执行!
    }
}

输出:

接住了:/ by zero
程序结束

执行流程(对着上面代码走一遍):

  1. try 里的代码正常执行——没出事,catch 整个跳过,直接跑到后面;
  2. 一旦出事(10 / 0 炸出 ArithmeticException 对象):try 里剩余代码立刻不跑,JVM 拿着异常对象来匹配 catch 括号里的类型;
  3. 类型对上 → 进入 catch,变量 e 接住这个对象——e.getMessage() 取事故消息;
  4. catch 跑完 → 程序继续,后面的代码照常执行。

对照 5.1:同一行 10 / 0,没有 try-catch 时"程序结束"永远打不出来;现在程序体面地跑完了。catch 的类型匹配本质是 4.4 的多态:e 声明为 ArithmeticException 类型,实际接住的是它的对象——"编译看左,运行看右"在异常这又上岗了。

2. catch 的匹配规矩

规矩一:类型要接得住。catch 里写的是异常类型,抛出对象必须是它的类型(或子类)才接得住——接不住就当没写,照样炸:

try {
    int r = 10 / 0;
} catch (NullPointerException e) {   // 拿空指针的手套去接算术异常
    System.out.println("接住了");
}
// 结果:炸。ArithmeticException 不是 NullPointerException,接不住

规矩二:多个 catch 从上到下挨个试,命中即走(只进一个):

try {
    int[] arr = new int[3];
    arr[5] = 1;
} catch (ArithmeticException e) {
    System.out.println("算术问题");
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("越界了");      // 命中这条
} catch (Exception e) {
    System.out.println("兜底");
}

规矩三(大坑):父类不能写在子类前面——Exception 是所有异常的父类,放第一个就把所有异常全接了,后面的子类 catch 永远轮不到。编译器直接报错:"已捕获到异常"。正确顺序:子类在前,父类(Exception)殿后兜底。

📝 混脸熟:多类型合并

JDK 7 起可以把两个 catch 合成一个:catch (ArithmeticException | NullPointerException e)——两条类型共用一个预案,分隔符是 |。先会认,用得多了自然熟。

3. finally:收尾专用,无论如何都跑

try {
    int r = 10 / 0;                        // 炸
} catch (ArithmeticException e) {
    System.out.println("接住");
} finally {
    System.out.println("finally:收尾工作");
}

finally 里的代码两条路线都跑:没出事 → try 跑完跑 finally;出了事 → catch 跑完跑 finally。连 return 都拦不住它。

用途一句话:无论结局如何都必须做的事——关文件、断开网络、释放资源、恢复状态。对照第 1 节:万一炸了,"释放资源"的代码可不能跟着一起跳过——放进 finally 就跑不丢。

4. throw 与 throws:甩锅系

catch 是"接",另两个关键词管"甩":

throw:主动抛出(动作)

写在方法体里,后面跟一个异常对象——意思是"这里出问题了,我现在就甩出去":

static void setScore(int score) {
    if (score < 0 || score > 100) {
        // 3.3 的 setter 打印升级版:安检不过,直接甩异常
        throw new IllegalArgumentException("分数必须在 0~100 之间");
    }
    this.score = score;
}

throws:声明甩出(声明)

写在方法签名的括号后面,后面跟异常类名(可多个)——意思是"我这个方法可能会甩这种异常,我不管,谁调用谁负责":

static void readFile() throws FileNotFoundException {
    // 方法体里不用写预案——编译器看签名上有声明,放行
}

两者对照(差一个 s,差很远)

维度 throw throws
写在哪 方法体里(语句) 方法签名括号后
后面跟什么 一个异常对象(new 出来的) 异常类名(可多个,逗号隔开)
干什么 执行到这行才真的甩 提前声明"可能甩",不甩也行
后果 抛出后方法立刻中断 调用者必须接(try-catch)或继续甩(throws)

5.2 的 Unhandled exception 至此有了两条活路:受检异常要么 try-catch 接住,要么方法签名 throws 甩出去。但甩不是无限的——一层层甩到 main 还没人接,就走 5.1 的老路:打印事故报告,程序中断。

💡 主动甩的价值:把"打印一句"升级成"真正报警"

3.3 的 setter 安检不过只是 System.out.println 提醒一句,调用者不理照样继续跑;throw 甩出异常后,方法立刻中断、调用者必须面对——安检从"贴告示"升级成"拉闸"。异常不只是防事故,也是代码表达"这不行"的正式方式。

5. 动手:一套完整的接与甩

在 IDEA05 新建 Demo03(照第 1 节改造版)和下面这个综合版:

public class Demo04 {
    public static void main(String[] args) {
        try {
            int r = divide(10, 0);              // 调用会甩异常的方法
            System.out.println("结果:" + r);    // 不执行
        } catch (ArithmeticException e) {
            System.out.println("接住:" + e.getMessage());
        } finally {
            System.out.println("finally:收尾");
        }
        System.out.println("程序继续跑");
    }

    static int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("除数不能为零");  // 主动甩
        }
        return a / b;
    }
}

输出:

接住:除数不能为零
finally:收尾
程序继续跑

三个 30 秒实验

  1. 接不住照样炸:把 catch 类型改成 NullPointerException——编译不报错,运行时还是炸(catch 匹配失败)。
  2. 父类在前报错:再加一个 catch (Exception e) 放在 ArithmeticException 前面——编译报错"已捕获到异常"。
  3. return 拦不住 finally:try 块末尾加 return;,finally 里的打印照样执行。

6. 常见误区

⚠️ 误区 1:try 块越大越好,干脆包住整个 main

try 圈的代码越多,catch 时越不知道是哪行出的事,也无谓地扩大了"中断范围"。精确圈住风险代码,try 块小而准才是好写法。

⚠️ 误区 2:catch (Exception e) 一把梭最省事

编译器不拦,但所有异常被同一个预案处理=任何具体信息都被抹平,真出了 bug 你只有一句"出错了"。接具体的异常类型,Exception 只当最后兜底。

⚠️ 误区 3:finally 一定执行

差一点是一定——System.exit() 强制退出 JVM 时 finally 不会跑(会认即可)。日常写代码可以放心当它"必跑"。

⚠️ 误区 4:throw 和 throws 差一个 s,是同一个东西

一个是动作(方法体里真的抛,后跟异常对象),一个是声明(方法签名上预告,后跟异常类名)。考试和面试的经典辨析题。

⚠️ 误区 5:throw 后面想抛什么就抛什么

只能抛 Throwable 家族的对象(Error / Exception 及其子类)——抛个 String 或自定义的普通类,编译报错。想甩自己的异常?得先建一个继承 Exception 的类(自定义异常,后面章节见)。

⚠️ 误区 6:异常被 catch 接住就不用改 bug 了

catch 是预案不是遮羞布。空指针被接住了,null 还在——该判空判空,该验货验货。5.2 的老话:能靠改代码根除的,改代码。

7. 自测题(先自己答,再展开)

❓ Q1:try 块里出了异常,执行路线怎么走?

炸点之后的 try 剩余代码立刻不跑,JVM 拿异常对象从上到下匹配 catch 类型 → 对上就进 catch 执行 → finally → 继续跑后面的代码。没出事则 catch 整个跳过。程序不再中断。

❓ Q2:多个 catch 的书写顺序有什么规矩?违反会怎样?

子类在前、父类(Exception)殿后。父类写在子类前面会编译报错"已捕获到异常"——父类把所有异常都接走了,子类 catch 成了永远轮不到的死代码。

❓ Q3:finally 什么时候用?它保证什么?

收尾专用:无论 try 里炸不炸、catch 接没接住,finally 都执行(return 也拦不住)。保证"关文件、释放资源"这类必须做的事不因异常而漏做。

❓ Q4:throw 和 throws 的区别?

throw 是动作:方法体里,后跟一个 new 出来的异常对象,执行到才真的甩,方法立刻中断;throws 是声明:方法签名括号后,后跟异常类名(可多个),预告"可能甩",逼调用者决定接还是继续甩。

❓ Q5(毕业题):把 3.3 的 Student.setScore 安检升级——分数不在 0~100 时 throw IllegalArgumentException,并在测试类的 main 里 try-catch 调用,finally 打印"录入结束"。
public void setScore(int score) {
    if (score < 0 || score > 100) {
        throw new IllegalArgumentException("分数必须在 0~100 之间");
    }
    this.score = score;
}
// 测试类:
try {
    s.setScore(-5);
} catch (IllegalArgumentException e) {
    System.out.println("录入被拦:" + e.getMessage());
} finally {
    System.out.println("录入结束");
}

合格标准:编译运行通过,负分被拦、程序不中断、"录入结束"必打印。优秀标准:能说清 throw(安检拉闸)与 3.3 版 println(贴告示)的本质区别——一个中断方法逼调用者面对,一个打印完若无其事继续跑。

8. 下一步学什么


相关笔记