方法重写时,子类不得扩大检查异常范围,仅可缩小或不声明;运行时异常不受约束;异常捕获以编译时类型为准,保障多态安全。

方法重写时的异常声明规则,核心是保障多态调用下“编译时可知、运行时可控”,不是语法枷锁,而是契约底线。关键在于:子类不能扩大检查异常范围,否则调用方按父类契约写的代码会漏捕异常,导致运行时崩溃。
明确检查异常与运行时异常的分界
Java 中只有 检查异常(checked exception) 受重写规则约束;运行时异常(RuntimeException 及其子类) 完全自由。
- 父类方法声明
throws IOException,子类可写throws FileNotFoundException(它是IOException的子类),也可不写 throws - 父类没声明任何检查异常,子类覆盖时就不能加
throws SQLException或throws IOException - 但子类可以自由抛出
IllegalArgumentException、NullPointerException,哪怕父类从不抛——编译器不管
紧扣变量编译时类型决定异常处理边界
异常能否被 catch,只看引用的声明类型,不看实际对象类型。这是多态安全的基石,也是最容易踩坑的地方。
-
InputStream is = new FileInputStream("a.txt"); is.read();—— 编译器只认InputStream.read()声明的IOException,你不能直接catch FileNotFoundException - 即使
FileInputStream.read()内部可能因路径不存在而抛FileNotFoundException,调用方也必须按父类契约来捕获或声明 - 若父类方法声明
throws Exception,子类只能缩小(如throws IOException),绝不能扩大为throws Throwable
当业务需要更多错误语义,绕开 throws 扩展
硬加检查异常违反契约?那就换方式表达错误意图,保持接口稳定又不失表达力。
- 用自定义 运行时异常 封装新语义,例如
ValidationFailedException、BusinessRuleViolationException,调用方按需捕获,不破坏现有 try-catch 结构 - 在 Javadoc 明确写清:“本方法在输入为空时抛出
IllegalArgumentException”,形成隐式契约,让使用者有据可依 - 前置校验 + 断言替代部分异常,比如
Objects.requireNonNull(param, "param must not be null"),把问题拦截在入口,避免异常流扩散 - 对关键链路,考虑返回
Result<T, E>类型(如 Vavr 或自定义),让错误成为值的一部分,强制调用方显式处理
典型误用场景与修正示例
常见错误是“想当然”加异常,结果破坏多态兼容性:
- ❌ 错误:父类
public void save() {},子类写public void save() throws SQLException {}→ 编译失败 - ✅ 正确:子类改用
throw new RuntimeException(new SQLException(...))包装,或定义DbSaveFailureException extends RuntimeException - ❌ 错误:父类
throws IOException,子类改写为throws Exception→ 违反“不能扩大”原则 - ✅ 正确:子类保持
throws IOException,或细化为throws EOFException(它是IOException子类)

















