方法覆盖时子类不能抛出比父类更宽泛的检查异常,这是里氏替换原则在异常层面的体现,旨在保障多态调用安全、维护接口契约;运行时异常不受此限。

方法覆盖时,子类不能抛出比父类更宽泛的检查异常,这是面向对象中契约约束的直接体现。它不是语法限制,而是为了保障多态调用的安全性——调用方只依赖父类声明,必须能合理应对所有可能的异常情形。
覆盖方法的异常声明规则
Java 中,子类重写父类方法时,其 throws 子句只能缩小或保持不变,不能扩大:
- 父类方法声明 throws IOException,子类可声明 throws FileNotFoundException(是 IOException 的子类),也可不写 throws
- 父类未声明 throws 任何检查异常,子类覆盖时也不能声明 throws 检查异常(如 IOException、SQLException)
- 运行时异常(RuntimeException 及其子类)不受此限,子类可自由添加或移除
为什么禁止扩大异常范围?
本质是里氏替换原则(LSP)在异常层面的延伸。若子类擅自增加新检查异常:
- 已有调用代码(只捕获父类声明的异常)将无法处理新增异常,导致编译通过但运行时崩溃
- 破坏接口契约:父类承诺“可能出现 A 类异常”,子类却引入 B 类异常,调用方无法预知和防御
- 削弱多态价值:无法安全地用子类对象替换父类引用,违背继承设计初衷
变量类型与异常处理的关系
异常能否被正确捕获,取决于引用的编译时类型,而非实际对象类型:
- Parent p = new Child(); p.method(); —— 编译器只认 Parent.method() 声明的异常
- 即使 Child.method() 实际抛出 SQLException,只要 Parent.method() 没声明,这段代码就无法编译通过(若尝试 catch SQLException)
- 若 Parent.method() 声明 throws Exception,则调用方需按 Exception 或其子类处理,Child 可选择抛更具体的子类,但不能抛 Exception 的父类(如 Throwable)
契约约束下的实用建议
当业务逻辑确实需要更多错误反馈时,应绕过 throws 扩展,转而强化契约表达:
- 用自定义运行时异常封装新语义(如 ValidationFailedException),避免检查异常限制
- 在方法文档或 Javadoc 中明确说明“可能因数据不满足前置条件而抛出 XXX”,形成隐式契约
- 配合断言或契约式编程(如 C++26 的 [[expects]] 或 Java 中的 Objects.requireNonNull)提前拦截非法输入,把错误控制在调用入口
- 对关键操作,优先使用返回结果对象(如 Result
)替代异常传递错误,使错误处理显式化、类型安全










