☕ 第五章 异常
📄 一句话概括

自定义异常 = 印一张自己的「事故报告」:继承 Exception 造类、补构造方法接消息、throw 上岗安检——它不是新语法,只是「继承 + throw/throws」的组合技。

💡 先修课

4.1 继承(extends 的规矩)、5.3 异常处理及语法(throw 与 throws,误区里埋的钩子这节收网)、5.4 抛出异常(甩锅链条)。


1. 为什么需要"自定义"异常?

5.3 的安检版本我们这样甩:

if (age < 0 || age > 150) {
    throw new Exception("年龄必须在 0~150 之间!");
}

功能没问题,但有两个别扭:

别扭点 说明
说普通话 Exception 只能靠字符串区分"这是年龄问题还是余额问题",万一忘了写消息,抓瞎
catch 没法精准匹配 catch (Exception e) 什么都能接——想单独处理"年龄不合法"?接不住,一接全接

就像医院挂号:所有病人都写"不舒服",医生没法分诊。自定义异常 = 给每一种业务问题发一张专属病历,名字自带行话,catch 能精准分诊。

2. 诞生三步

以"年龄越界"为例:

第一步:继承 Exception(或 RuntimeException)

// 受检版:编译器会逼调用者表态(适合"外部风险"型问题)
public class AgeOutOfBoundsException extends Exception {
}

类名惯例:以 Exception 结尾,一眼认出这是异常类。继承谁决定待遇(收网 5.2 的两派知识):

第二步:补两个构造方法

public class AgeOutOfBoundsException extends Exception {
    public AgeOutOfBoundsException() {
        super();          // 无参:不携带消息
    }
    public AgeOutOfBoundsException(String message) {
        super(message);   // 带参:把消息上交给父类存档
    }
}
❓ 为什么一定要写 super(message)?

消息的旅程是:throw new Xxx("消息") → 你的构造方法收到 message → super(message) 上交给 Exception 父类,存进对象内部的 detailMessage 字段。将来 e.getMessage() 取的就是父类存的这份。 不写 super(message),消息就断在半路,getMessage() 返回 null——本节第一大坑。

第三步:throw 上岗 + catch 接住

public class Person {
    private int age;

    public void setAge(int age) throws AgeOutOfBoundsException {   // 声明:可能甩出
        if (age < 0 || age > 150) {
            throw new AgeOutOfBoundsException("年龄 " + age + " 不合法,必须在 0~150 之间");
        }
        this.age = age;
    }
}

public class Demo06 {
    public static void main(String[] args) {
        Person p = new Person();
        try {
            p.setAge(-5);
        } catch (AgeOutOfBoundsException e) {          // 精准分诊:只接这一种
            System.out.println("安检拦截:" + e.getMessage());
        }
    }
}
安检拦截:年龄 -5 不合法,必须在 0~150 之间

对比 5.3 的版本:catch 里只接"年龄问题",别的异常该炸还炸——分诊能力就是自定义异常的核心价值。

3. 消息传递链(一图流)

「消息」从 throw 到 getMessage 的完整旅程:

  1. 甩出:throw new AgeOutOfBoundsException("年龄-5不合法!") —— 消息塞进构造方法
  2. 接住:你的构造方法参数 String message 收下
  3. 上交:super(message) → Exception 父类构造把它存进 detailMessage 字段(看不见但一直在)
  4. 取货:catch 里 e.getMessage() 从父类方法里取回
📝 为什么"空壳类"也能 new?

第二步如果偷懒不写构造方法,new AgeOutOfBoundsException() 也能编译通过——因为不写构造方法时类自动继承父类的无参构造(3.4 构造方法 的规矩)。但这时带消息的版本用不了,throw new Xxx("消息") 直接编译报错。所以规范写法是两个构造都补齐。

4. 受检还是运行时?选型口诀

继承谁 待遇 适合场景
extends Exception 受检,编译器逼处理 调用者有能力预防或补救的业务风险:余额不足、年龄越界、文件不存在
extends RuntimeException 运行时,随缘处理 属于使用者的 bug:传参传错了、非法状态——改代码才是正解

新手期一句话:拿不准就继承 Exception(受检)——编译器会替你检查每个调用者有没有表态,最不容易漏。

5. 动手环节(IDEA05 · Demo06)

  1. 新建包 com.itheima.exception05,先写 AgeOutOfBoundsException(继承 Exception + 两个构造)。
  2. 写 Person.setAge(throws + throw 自定义异常),main 里 try-catch 打印 e.getMessage()。成功标准:控制台打印"安检拦截:年龄 -5 不合法…"。
  3. 30 秒实验 ①:把 AgeOutOfBoundsException(String message) 里的 super(message) 注释掉 → 编译通过,但 getMessage() 打印 null。
  4. 30 秒实验 ②:改成继承 RuntimeException → setAge 的 throws 和 main 的 try-catch 全删也能编译(运行时待遇,收网 5.2)。
  5. 30 秒实验 ③:再定义一个 ScoreOutOfBoundsException,在 main 里故意只 catch 它却触发年龄异常 → 按住 Ctrl 看编译器报错——catch 接不住的受检异常,编译器照样拦。

6. 常见误区

⚠️ 五大误区
  1. 忘了 super(message) → 消息断传,getMessage() 返回 null。这是第一大坑,30 秒实验 ① 亲眼看过一次就记住了。
  2. 以为自定义异常是"新语法" → 不是。它就是 4.1 继承 + 5.3 throw/throws 的组合技,没有任何新关键词。
  3. catch 自定义异常时以为"名字像就能接" → catch 按家谱匹配(5.3 规矩),类型对不上照样接不住。
  4. 继承 Exception 却忘了 throws → 受检异常甩出必须声明,编译器当场报 Unhandled exception。
  5. 到处自定义、滥用异常 → 简单的参数错误用 IllegalArgumentException 之类的现成异常就够了;自定义留给业务专属、需要分诊的问题。

7. 本节小结

要点 内容
是什么 继承 Exception/RuntimeException 造的业务专属异常类
三步 继承 → 补构造(super(message) 上交消息)→ throw 上岗
核心价值 catch 精准分诊 + 报错信息自带行话
选型 业务风险 → Exception;使用者的 bug → RuntimeException
命名 XxxException 后缀

8. 可折叠自测题

❓ Q1:自定义异常类继承 Exception 和继承 RuntimeException,调用方写法有什么区别?

继承 Exception(受检):调用方必须 try-catch 或 throws,否则编译报错;继承 RuntimeException(运行时):调用方写不写处理都能编译,不处理则运行时炸。

❓ Q2:`throw new AgeOutOfBoundsException("消息")` 编译报错,最可能的原因?

自定义类里没写带 String 参数的构造方法。空壳类只自动继承父类无参构造,带消息的版本得自己补 Xxx(String message) { super(message); }。

❓ Q3:getMessage() 返回 null,排查思路?

先查构造方法里是不是漏了 super(message)——消息没上交给父类存档;再确认甩出时确实传了消息而不是 new Xxx()。

❓ Q4:写一个"余额不足异常",类和甩出处的代码长什么样?
public class BalanceNotEnoughException extends Exception {
    public BalanceNotEnoughException() { super(); }
    public BalanceNotEnoughException(String message) { super(message); }
}

public void withdraw(double amount) throws BalanceNotEnoughException {
    if (amount > balance) {
        throw new BalanceNotEnoughException("余额不足:当前 " + balance + ",请求 " + amount);
    }
    balance -= amount;
}
❓ Q5(毕业题):一句话说清自定义异常在整个异常体系里的位置

它是 Exception/RuntimeException 家谱上的新枝叶——用继承挂上去,用 throw 甩出,用 catch 精准接住;甩锅链条、两派待遇、消息机制全部照用父类那一套。


9. 下一步学什么


相关笔记