throws 与 throw
本节定位:明确 throws 和 throw 两个关键字在语法、语义和使用场景上的本质区别。掌握用 throws 声明方法契约、用 throw 手动抛出异常对象,以及异常链机制如何实现根因追溯。
场景引入
"白歌,我这里有个问题。"飞翔科技的小崔指着自己的代码,"我在方法签名字写了个 throws IOException,但同事说我还应该把 SQLException 也声明上去,到底什么情况下该用 throws?"
架构师白歌走过来看了一眼:"throws 和 throw 是两回事。throws 是告诉调用方'我这个方法可能会出问题',throw 是你在方法里面主动说'现在出问题了'。一个是声明,一个是动作。"
旁边的李眉也凑过来:"那什么时候用哪个?我经常搞混。"
大翔笑道:"记住一句话——throws 在方法签名上(声明契约),throw 在方法体里面(抛出动作)。一个是承诺书,一个是行动。"
定义对比
| 维度 | throws | throw |
|---|---|---|
| 关键字类型 | 方法声明的一部分 | 可执行语句 |
| 位置 | 方法签名中,参数列表后、方法体前 | 方法体内部 |
| 作用 | 声明该方法可能抛出哪些异常 | 真正抛出一个异常对象 |
| 跟随内容 | 异常类名(可以多个,逗号分隔) | 异常对象(new 出来的实例) |
| 对调用方影响 | 调用方必须 try-catch 或继续 throws | 无直接影响(但异常会向上传播) |
| 编译行为 | 编译器检查调用方是否处理了受检异常 | 编译器不强制检查 |
throws:方法契约的异常声明
语法与使用
修饰符 返回类型 方法名(参数列表) throws 异常类型1, 异常类型2, ... {
// 方法体
}
/**
* 从数据库中查询学生信息
*
* @param studentId 学生ID
* @return 学生对象
* @throws SQLException 数据库操作异常(受检异常,调用方必须处理)
* @throws IllegalArgumentException 学生ID为空(非受检异常,声明是为了文档化)
*/
public Student findStudent(String studentId)
throws SQLException, IllegalArgumentException {
if (studentId == null || studentId.isEmpty()) {
throw new IllegalArgumentException("学生ID不能为空"); // 非受检,可以不声明
}
// JDBC 操作可能抛出 SQLException(受检异常,必须声明或捕获)
Connection conn = DataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM students WHERE id = ?");
ps.setString(1, studentId);
ResultSet rs = ps.executeQuery();
// ...
}
throws 的继承规则
当子类重写父类方法时,throws 声明遵循严格的规则:
| 规则 | 说明 | 示例 |
|---|---|---|
| 不能抛出更宽泛的异常 | 子类不能声明父类方法未声明的新的受检异常 | 父类方法无 throws,子类不能加 throws IOException |
| 可以抛出更具体的异常 | 子类可以声明父类异常的子类 | 父类 throws Exception,子类可以 throws IOException |
| 可以不声明异常 | 子类可以不抛任何异常 | 父类 throws IOException,子类可以不 throws |
| 可以增加非受检异常 | 子类可以增加任意 RuntimeException | 不受限制 |
// 父类接口
interface StudentRepository {
Student findById(String id) throws DataAccessException;
}
// ✅ 正确:不声明异常
class InMemoryStudentRepo implements StudentRepository {
@Override
public Student findById(String id) { // 可以不 throws
return storage.get(id);
}
}
// ✅ 正确:声明更具体的异常
class DatabaseStudentRepo implements StudentRepository {
@Override
public Student findById(String id) throws SQLException { // SQLException 是 DataAccessException 的子类
// JDBC 操作
}
}
// ❌ 编译错误:声明了更宽泛或无关的受检异常
class BrokenStudentRepo implements StudentRepository {
@Override
public Student findById(String id) throws Exception { // 编译错误!Exception 比 DataAccessException 宽泛
// ...
}
}
throw:手动抛出异常
语法
throw 异常对象实例;
throw 是一个语句,必须跟一个 Throwable 的实例(通常是 new 出来的)。
使用场景
场景一:参数校验
public void enrollStudent(Student student) {
// 防御性编程:参数校验
if (student == null) {
throw new IllegalArgumentException("学生对象不能为 null");
}
if (student.getName() == null || student.getName().trim().isEmpty()) {
throw new IllegalArgumentException("学生姓名不能为空");
}
if (student.getAge() < 6 || student.getAge() > 100) {
throw new IllegalArgumentException("学生年龄不合法: " + student.getAge());
}
// 业务逻辑
studentDao.save(student);
}
场景二:业务规则违反
public void transferCredits(String fromStudentId, String toStudentId, int amount) {
Student from = studentDao.findById(fromStudentId);
Student to = studentDao.findById(toStudentId);
if (from == null) {
throw new StudentNotFoundException("转出学生不存在: " + fromStudentId);
}
if (to == null) {
throw new StudentNotFoundException("转入学生不存在: " + toStudentId);
}
if (from.getCredits() < amount) {
throw new InsufficientCreditsException(
"学分不足,需要 " + amount + ",当前只有 " + from.getCredits());
}
from.setCredits(from.getCredits() - amount);
to.setCredits(to.getCredits() + amount);
studentDao.update(from);
studentDao.update(to);
}
场景三:异常转换(包装)
将底层异常包装为上层有意义的异常:
public List<Student> importFromFile(String filePath) throws StudentImportException {
try {
List<String> lines = Files.readAllLines(Paths.get(filePath));
return parseStudents(lines);
} catch (IOException e) {
// 将底层 IOException 包装为业务异常
throw new StudentImportException("导入学生数据失败: " + filePath, e);
}
}
异常链:追溯根因
什么是异常链
异常链是指将一个异常(原始原因 cause)关联到另一个异常(上层包装),形成一条从表层到根因的追溯链。
两种设置 cause 的方式
// 方式一:构造方法传入 cause(推荐)
throw new StudentImportException("导入失败", ioException);
// 方式二:通过 initCause() 方法(已废弃的用法,仅了解)
StudentImportException ex = new StudentImportException("导入失败");
ex.initCause(ioException); // 只能调用一次
throw ex;
自定义异常支持异常链
// 自定义异常需要提供接收 cause 的构造方法
public class StudentImportException extends Exception {
public StudentImportException(String message, Throwable cause) {
super(message, cause); // 调用 Throwable(String, Throwable) 构造器
}
}
完整示例:异常链的追踪
public class ExceptionChainDemo {
// 底层方法:抛出原始异常
public static String readFileContent(String path) throws IOException {
throw new FileNotFoundException("文件不存在: " + path); // 根因
}
// 中间层:包装原始异常
public static String parseConfig(String path) throws ConfigException {
try {
return readFileContent(path);
} catch (IOException e) {
throw new ConfigException("配置加载失败", e); // 包装,保留根因
}
}
// 上层:再次包装
public static void initSystem() throws SystemInitException {
try {
String config = parseConfig("/etc/app/config.properties");
System.out.println("配置内容: " + config);
} catch (ConfigException e) {
throw new SystemInitException("系统初始化失败", e);
}
}
public static void main(String[] args) {
try {
initSystem();
} catch (SystemInitException e) {
// 打印完整异常链
System.err.println("=== 异常链分析 ===");
System.err.println("表层异常: " + e);
Throwable cause = e.getCause();
int level = 1;
while (cause != null) {
System.err.println("原因层级 " + level + ": " + cause);
cause = cause.getCause();
level++;
}
// 打印完整堆栈(包含所有层级的 suppressed 和 cause)
System.err.println("\n=== 完整堆栈 ===");
e.printStackTrace();
}
}
}
// 自定义异常类(支持异常链)
class ConfigException extends Exception {
public ConfigException(String message, Throwable cause) {
super(message, cause);
}
}
class SystemInitException extends Exception {
public SystemInitException(String message, Throwable cause) {
super(message, cause);
}
}
运行输出:
=== 异常链分析 ===
表层异常: SystemInitException: 系统初始化失败
原因层级 1: ConfigException: 配置加载失败
原因层级 2: java.io.FileNotFoundException: 文件不存在: /etc/app/config.properties
=== 完整堆栈 ===
SystemInitException: 系统初始化失败
at ExceptionChainDemo.initSystem(ExceptionChainDemo.java:21)
...
Caused by: ConfigException: 配置加载失败
at ExceptionChainDemo.parseConfig(ExceptionChainDemo.java:13)
...
Caused by: java.io.FileNotFoundException: 文件不存在: /etc/app/config.properties
at ExceptionChainDemo.readFileContent(ExceptionChainDemo.java:6)
...
异常传播机制
当一个异常在方法中抛出但未被捕获时,异常会沿着调用栈向上传播。
传播规则:
- 方法抛出异常 → JVM 查找当前方法的异常表
- 如果异常表中有匹配的 catch → 跳转到 catch 块
- 如果没有匹配的 catch → 方法立即结束,异常抛给调用方
- 调用方重复步骤 1-3
- 如果 main() 也没有捕获 → 线程终止,JVM 打印异常栈
public class PropagationDemo {
public static void level3() {
System.out.println("进入 level3");
throw new RuntimeException("level3 抛出的异常");
}
public static void level2() {
System.out.println("进入 level2");
level3(); // 不捕获,向上传播
System.out.println("离开 level2"); // 这行不会执行
}
public static void level1() {
System.out.println("进入 level1");
try {
level2();
} catch (RuntimeException e) {
System.out.println("level1 捕获异常: " + e.getMessage());
// 可以在这里重新抛出或处理
}
System.out.println("离开 level1"); // 这行会执行(因为异常被捕获了)
}
public static void main(String[] args) {
System.out.println("=== 异常传播演示 ===");
level1();
System.out.println("程序正常结束");
}
}
运行输出:
=== 异常传播演示 ===
进入 level1
进入 level2
进入 level3
level1 捕获异常: level3 抛出的异常
离开 level1
程序正常结束
飞翔科技实战:学生管理系统中的异常契约
场景
架构师白歌定义了学生管理服务的异常契约:
/**
* 学生管理服务接口
* 方法签名中的 throws 就是双方约定的"异常契约"
*/
public interface StudentService {
/**
* 注册新学生
* @throws DuplicateStudentException 学号已存在
* @throws InvalidStudentException 学生信息不合法
*/
void registerStudent(Student student)
throws DuplicateStudentException, InvalidStudentException;
/**
* 导入学生数据
* @throws StudentImportException 导入过程中的任何异常
*/
int importStudents(InputStream data)
throws StudentImportException;
}
小崔的实现:
@Service
public class StudentServiceImpl implements StudentService {
@Override
public void registerStudent(Student student)
throws DuplicateStudentException, InvalidStudentException {
// 1. 参数校验:抛出非受检异常或受检异常
if (student == null) {
throw new IllegalArgumentException("学生对象不能为空"); // 非受检
}
if (student.getStudentId() == null) {
throw new InvalidStudentException("学号不能为空"); // 受检,已声明
}
// 2. 业务规则校验
Student existing = studentDao.findByStudentId(student.getStudentId());
if (existing != null) {
throw new DuplicateStudentException(
"学号 " + student.getStudentId() + " 已被 " + existing.getName() + " 使用");
}
// 3. 数据库操作
try {
studentDao.save(student);
logger.info("学生注册成功: {} - {}", student.getStudentId(), student.getName());
} catch (DataAccessException e) { // Spring 的数据访问异常(非受检)
// 包装为受检的业务异常,保留根因
throw new StudentServiceException("保存学生信息失败", e);
}
}
@Override
public int importStudents(InputStream data) throws StudentImportException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(data))) {
int count = 0;
String line;
while ((line = reader.readLine()) != null) {
Student student = parseStudent(line);
registerStudent(student); // 可能抛出 DuplicateStudentException
count++;
}
return count;
} catch (IOException e) {
throw new StudentImportException("读取导入数据失败", e);
} catch (DuplicateStudentException e) {
throw new StudentImportException("导入数据包含重复学号", e);
}
}
private Student parseStudent(String line) throws InvalidStudentException {
String[] parts = line.split(",");
if (parts.length < 3) {
throw new InvalidStudentException("数据格式错误,需要至少3个字段: " + line);
}
return new Student(parts[0], parts[1], Integer.parseInt(parts[2]));
}
}
易错场景
反例一:throws 了不该声明的非受检异常
// ❌ 错误:在 throws 中声明 RuntimeException 是不必要的
// 这样做会误导调用方以为需要处理
public void doSomething() throws NullPointerException {
// NullPointerException 是 RuntimeException,不应该声明
}
// ❌ 更糟糕:声明了但实际不处理
public void caller() {
try {
doSomething();
} catch (NullPointerException e) {
// 用 try-catch 处理 NPE 而不是修复代码逻辑
System.out.println("NPE 发生了");
}
}
// ✅ 正确:非受检异常不应该在 throws 中声明
// (除非是为了文档化目的,但不应强制调用方处理)
public void doSomething() {
// 通过代码逻辑防御 NPE
// 如果真的想文档化,用 @throws javadoc 标签而非方法签名中的 throws
}
反例二:throw 了不应该抛的异常
// ❌ 错误:用异常做正常流程控制
public Student findStudent(String id) {
Student student = studentDao.findById(id);
if (student == null) {
throw new StudentNotFoundException("学生不存在: " + id);
// 异常不应该用于可预见的正常流程
}
return student;
}
// ✅ 正确方案一:返回 Optional(JDK 8)
public Optional<Student> findStudent(String id) {
return Optional.ofNullable(studentDao.findById(id));
}
// ✅ 正确方案二:返回 null 并文档化
/**
* @return 学生对象,如果不存在返回 null
*/
public Student findStudent(String id) {
return studentDao.findById(id);
}
经验法则:异常应该用于"异常情况"——不应该用于可预见的、正常的业务流程控制。查找不到学生是正常情况,不应该用异常。
反例三:重新抛出时丢失原始异常
// ❌ 错误:包装异常时没有传入 cause
try {
studentDao.save(student);
} catch (SQLException e) {
// 丢失了原始异常的堆栈信息
throw new StudentServiceException("保存失败");
}
// ✅ 正确:保留原始异常作为 cause
try {
studentDao.save(student);
} catch (SQLException e) {
throw new StudentServiceException("保存失败", e); // 传入 cause
}
面试考点
Q1:throws 和 throw 有什么区别?
throws 是方法声明的一部分,写在方法签名中,声明该方法可能抛出的异常类型(可声明多个),用于告知调用方需要处理哪些异常。throw 是方法体内的可执行语句,用于实际抛出一个异常对象实例。记忆口诀:throws 是"声明",throw 是"动作"。
Q2:子类重写父类方法时,throws 声明有什么限制?
子类重写方法不能声明比父类方法更宽泛的受检异常(即不能声明父类未声明的新的受检异常)。但子类可以:①不声明任何异常;②声明父类异常的子类;③声明任意非受检异常(RuntimeException)。这个规则保证了里氏替换原则——用子类替代父类时,调用方不会遇到意料之外的受检异常。
Q3:什么是异常链?为什么需要它?
异常链是指将一个异常(根因)关联到另一个异常(包装),通过
Throwable的cause属性形成链接。需要它的原因是:底层异常(如SQLException)对上层调用方没有业务意义,需要包装为业务异常(如StudentServiceException),但必须保留根因以便调试和日志分析。通过构造函数new Exception(message, cause)或initCause()方法设置。
Q4:什么时候用 throw,什么时候用 throws?
在方法体内部,需要主动报告错误时用 throw(如参数校验失败、业务规则违反)。在方法签名中,声明该方法可能抛出的受检异常时用 throws。原则:如果异常是方法无法处理的,就 throws 声明出去让调用方处理;如果能在方法内部处理,就 try-catch 而不用 throws。
Q5:throws 声明了 RuntimeException 会怎样?
编译不会报错,调用方也不会被强制处理。但在方法签名中声明 RuntimeException 会误导调用方以为这是一个需要处理的异常。Java 社区的最佳实践是:非受检异常不放在 throws 中(偶尔为了 javadoc 文档化用
@throws标签说明,但不在方法签名中强制声明)。