多态调用中异常处理须严格遵循接口契约:受检异常类型不可新增或变更,非受检异常触发条件与语义必须一致,禁止用异常替代业务状态,推荐用result/optional封装可恢复错误,测试需覆盖各实现类异常路径的一致性。

多态调用中异常处理的规范性,关键在于把异常作为行为契约不可分割的一部分——不是“要不要抛”,而是“按什么规则抛”。接口声明的方法一旦涉及异常,实现类必须严格遵循其类型、语义和触发条件,否则调用方在不感知具体实现的情况下就会崩溃。
接口需明确异常类型与语义边界
仅写 void save(User user) 不够,必须同步约定异常行为:
- 未声明受检异常(
throws IOException),实现类不得新增任何受检异常;否则调用方无法在编译期捕获,运行时直接中断 - 若接口已声明
throws DataAccessException,实现类可抛其子类(如JdbcUpdateException),但不能抛更宽泛的RuntimeException或无关异常(如NullPointerException) - 对非受检异常(
RuntimeException及其子类),契约应说明触发场景:例如“参数为 null 时抛IllegalArgumentException”,所有实现都必须一致,不能有的静默忽略、有的返回 false、有的抛IllegalStateException
实现类不得改变异常的“可预期性”
异常是调用方做错误处理的唯一依据。LSP 要求替换后逻辑流不变,异常策略也必须稳定:
- 前置条件收紧导致异常增多:接口允许
id ≥ 0,某实现却要求id > 100,原本合法的save(new User(5))就可能突然抛出IllegalArgumentException,破坏调用方假设 - 后置条件弱化导致异常消失:接口承诺“查不到用户时抛
UserNotFoundException”,某实现改为返回null,上层空指针风险立即引入 - 避免用异常替代业务状态:不要让
pay()在余额不足时抛InsufficientBalanceException,而另一实现返回false—— 这不是异常策略差异,是契约撕裂
用返回值封装可恢复错误,保留异常给真正异常
不是所有“失败”都该走异常路径。区分两类情况:
- 程序无法继续执行的意外(如数据库连接中断、磁盘满、序列化失败)→ 用受检或非受检异常表达,强制处理或传播
- 业务层面的预期分支(如用户不存在、库存不足、权限拒绝)→ 推荐用
Result<t></t>或Optional<t></t>封装,让调用方自主判断,不打断控制流 - 若坚持用异常表达业务结果,务必统一类型并写入接口 Javadoc,例如:
/** @throws UserDisabledException 若账户被冻结 */
测试要覆盖异常路径的多态一致性
单元测试不能只验证“正常流程”,必须验证异常是否按契约发生:
- 对每个实现类,编写相同输入(如传 null 参数、传非法 ID)的测试用例,断言抛出的异常类型和消息是否符合接口文档
- 使用接口类型调用,而非具体实现类名,确保测试的是多态行为本身
- 集成测试中模拟网络超时、DB 拒绝连接等真实异常场景,确认各实现类的异常包装层级一致(例如都包装为统一的
ServiceUnavailableException)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











