重写方法时受检异常必须遵循子类型约束:只能抛出父类声明的受检异常或其子类,不可新增无关异常;运行时异常无此限制,以保障多态调用安全和liskov替换原则。

重写方法时抛出受检异常(checked exception),必须遵循“异常子类型约束”:子类重写方法声明抛出的受检异常,只能是父类方法声明抛出的受检异常的子类,或不抛出任何新的受检异常;不能抛出父类方法未声明、且与之无关的受检异常。
只能缩小或保持受检异常范围
Java 编译器强制要求:重写方法的 throws 子句中列出的受检异常类型,必须是被重写方法 throws 子句中对应异常类型的子类型(包括相同类型),不能是父类型或无关类型。
- ✅ 允许:父类方法 throws IOException,子类重写方法 throws FileNotFoundException(它是 IOException 的子类)
- ✅ 允许:父类 throws SQLException,子类 throws SQLTimeoutException(JDBC 4.1+ 中它是 SQLException 子类)
- ❌ 不允许:父类 throws IOException,子类 throws SQLException(两者无继承关系)
- ❌ 不允许:父类方法没有 throws 任何受检异常,子类重写方法却 throws IOException
运行时异常(unchecked)不受此限制
RuntimeException 及其子类属于非受检异常,重写时可自由添加、删除或修改 throws 声明(甚至不写 throws),编译器不检查。例如子类方法可 throw NullPointerException 或自定义 RuntimeException,无需在父类中声明。
- 父类方法未声明异常 → 子类可 throw IllegalArgumentException(合法)
- 父类 throws IOException → 子类可额外 throw IllegalArgumentException(不需出现在 throws 列表中)
为什么有这个限制?
这是为了保障多态调用的安全性。假设变量声明为父类类型,实际指向子类对象,调用方只按父类方法签名做异常处理。若子类擅自抛出父类未声明的受检异常,调用方就无法在编译期预见并捕获,会破坏“异常必须显式处理”的契约。
- 代码示例:FileReader reader = new MyFileReader(); reader.read(); —— 调用方只按 FileReader.read() 的 throws IOException 处理;若 MyFileReader.read() 悄悄 throws ClassNotFoundException,编译将失败
- 本质是 Liskov 替换原则在异常层面的体现:子类型必须能安全地替换父类型,包括错误行为的契约一致性
常见误区提醒
注意区分“声明抛出”和“实际抛出”。即使子类方法体中 new 了某个受检异常对象,只要没在 throws 中声明,且该异常未被 try-catch 包裹,编译就会报错;反之,throws 列表中的异常,方法体里可以完全不 throw(只是承诺“可能”,非“一定”)。
- 子类方法 throws IOException,但内部只 throw new RuntimeException() → 合法
- 子类方法 throws IOException,但内部 throw new SQLException() → 编译报错(除非 SQLException 已声明在 throws 中,且是 IOException 的子类——但实际不是)
- 子类方法没写 throws,但写了 throw new IOException() → 编译报错:“unreported exception IOException”
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











