1. 引言
本文将深入解析 Java 中两个关键异常处理关键字:throw
和 throws
。它们虽然只差一个字母,但用途完全不同,稍不注意就容易踩坑。
我们将通过实际代码示例说明它们的使用场景,并总结一些生产环境中的最佳实践。目标是让你在面对异常设计时,能做出更合理的选择。
2. throw 与 throws 概述
这两个关键字都属于 Java 异常处理机制的核心部分。⚠️ 它们完全不能互换,理解其差异对写出健壮代码至关重要。
- ✅
throw
:用于主动抛出一个异常实例,通常出现在方法体内部 - ✅
throws
:用于方法签名中,声明该方法可能抛出的异常类型,告诉调用方“我这里可能会出问题”
异常的产生意味着程序正常流程被中断,比如用户输入非法数据、网络连接断开等。良好的异常处理机制能让系统在出错时优雅降级,而不是直接崩溃。
3. throw 的使用
throw
关键字用于在代码中显式抛出异常。它后面必须跟一个 Throwable
或其子类的实例。
来看一个经典的除零场景:
public double divide(double a, double b) {
if (b == 0) {
throw new ArithmeticException("除数不能为零!");
}
return a / b;
}
上面的例子中,当 b
为 0 时,我们主动抛出 ArithmeticException
,并附带清晰的错误信息。这是最基础但也最常见的用法。
3.1 最佳实践:使用具体异常
✅ 始终优先使用最具体的异常类型。例如:
- 遇到数字格式错误时,应抛
NumberFormatException
而不是笼统的IllegalArgumentException
- 不要直接抛
Exception
,这会让调用方无法精准捕获和处理
JDK 自身也遵循这一原则。比如 Integer.valueOf(String)
方法的声明:
public static Integer valueOf(String s) throws NumberFormatException
它明确表示:如果字符串格式不对,会抛 NumberFormatException
,而不是模糊地说“可能出错”。
✅ 更进一步的做法是定义自定义异常。比如我们可以创建一个专门用于除零的异常:
public class DivideByZeroException extends RuntimeException {
public DivideByZeroException(String message) {
super(message);
}
}
这样调用方可以针对性地捕获 DivideByZeroException
,逻辑更清晰。
3.2 异常包装(Exception Wrapping)
有时候我们不想让上层代码感知底层细节,就可以将原始异常包装成自定义异常。
先定义一个通用的数据访问异常:
public class DataAccessException extends RuntimeException {
public DataAccessException(String message, Throwable cause) {
super(message, cause);
}
}
假设有一个 DAO 方法可能抛出 SQLException
:
public List<String> findAll() throws SQLException {
throw new SQLException();
}
在服务层,我们可以将其包装并重新抛出:
public void wrappingException() {
try {
personRepository.findAll();
} catch (SQLException e) {
throw new DataAccessException("数据库查询失败", e);
}
}
✅ 这样做的好处:
- 上层业务代码无需了解
SQLException
这种底层细节 - 统一异常体系,便于集中处理
- 保留原始异常堆栈(通过传入
cause
),便于排查问题
测试验证:
@Test
void whenSQLExceptionIsThrown_thenShouldBeRethrownWithWrappedException() {
assertThrows(DataAccessException.class,
() -> simpleService.wrappingException());
}
3.3 多异常捕获(multi-catch)
Java 7 引入了 multi-catch 语法,可以简化多个异常的处理:
try {
tryCatch.execute();
} catch (ConnectionException | SocketException ex) {
System.out.println("网络相关异常");
} catch (Exception ex) {
System.out.println("其他异常");
}
⚠️ 注意:必须先捕获具体异常,再捕获通用异常。如果把 catch (Exception ex)
放在前面,后面的 catch 块将永远无法执行,编译器会报错。
4. throws 的使用
throws
出现在方法声明处,用于声明该方法可能抛出的异常类型。它可以声明多个异常,用逗号分隔。
例如:
public static void execute() throws SocketException, ConnectionException, Exception
调用该方法的代码必须处理这些异常,要么用 try-catch
捕获,要么继续向上抛。
4.1 受检异常 vs 非受检异常
Java 异常分为两大类,理解它们的区别是掌握 throws
的关键。
受检异常(Checked Exception)
✅ 在编译期就被检查的异常。编译器强制要求你处理,否则无法通过编译。
常见类型:
IOException
FileNotFoundException
ParseException
示例:
File file = new File("not_existing_file.txt");
try {
FileInputStream stream = new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
你也可以选择不处理,而是用 throws
向上抛:
private static void readFile() throws FileNotFoundException {
File file = new File("not_existing_file.txt");
FileInputStream stream = new FileInputStream(file);
}
⚠️ 但最终必须有人处理,否则程序会在运行时崩溃。
非受检异常(Unchecked Exception)
✅ 在运行期才抛出,编译器不强制检查。
常见类型:
NullPointerException
IllegalArgumentException
ArrayIndexOutOfBoundsException
示例:
public void runtimeNullPointerException() {
String a = null;
a.length(); // 运行时抛出 NullPointerException
}
测试验证:
@Test
void whenCalled_thenNullPointerExceptionIsThrown() {
assertThrows(NullPointerException.class,
() -> simpleService.runtimeNullPointerException());
}
✅ 关键区别总结:
类型 | 检查时机 | 是否必须处理 | 典型代表 |
---|---|---|---|
受检异常 | 编译期 | 是 | IOException |
非受检异常 | 运行期 | 否 | NullPointerException |
📌 技术细节:所有
RuntimeException
和Error
的子类都是非受检异常,其余Throwable
子类为受检异常。
5. 总结
- ✅
throw
用于抛出异常实例,在方法内部使用 - ✅
throws
用于声明异常类型,在方法签名中使用 - ✅ 优先使用具体异常,必要时定义自定义异常
- ✅ 合理使用异常包装,隔离底层细节
- ✅ 区分受检与非受检异常,理解编译器的强制要求
掌握这些知识点,能让你在设计 API 时更合理地暴露异常,避免调用方“一脸懵”。
源码示例已上传至 GitHub:https://github.com/tech-tutorial/core-java-exceptions
想深入了解 Java 异常体系,推荐阅读我们的专题文章《Java 异常处理全解析》