throw、throws 与异常传播 | JavaSE
throw、throws 与异常传播
一、学习目标
完成本章后,你应该能够:
- 能够区分
throw与throws的语法位置和职责。 - 能够使用
throw主动创建并抛出异常对象。 - 能够使用
throws声明方法可能向调用者传播的异常。 - 能够解释异常从当前方法向调用者逐层传播的过程。
- 能够理解 Checked Exception 与 RuntimeException 在
throws上的编译期差异。 - 能够分析多层方法调用中的异常传播路径。
- 能够判断一个异常应该“当前层处理”还是“继续向上层传播”。
- 能够避免
throws Exception、throws Throwable等过度宽泛的异常声明。
二、核心知识
2.1 从“捕获异常”到“传播异常”
上一章学习了:
try {
// 可能发生异常的代码
} catch (Exception e) {
// 当前层处理异常
}
这代表:
异常发生
↓
当前方法捕获
↓
当前方法负责处理
但实际开发中,并不是每一个方法都知道:
发生异常以后到底应该怎么办。
例如:
Controller
↓
UserService
↓
UserRepository
↓
读取文件 / 数据库 / 网络
假设最底层读取数据失败。
Repository 可能知道:
读取失败了
但是它并不知道最终应该:
给用户显示什么
返回什么状态码
是否允许重试
是否结束当前业务
这些决策往往应该由更上层完成。
因此 Java 还支持:
当前方法不处理
↓
把异常继续交给调用者
这就是:
异常传播(Exception Propagation)。
2.2 throws 是什么
throws 写在:
方法声明
上。
基本格式:
public static void method() throws 异常类型 {
}
例如:
public static void readData() throws Exception {
}
它表达的意思是:
当前方法可能产生这种异常,并允许异常继续向当前方法的调用者传播。
例如:
public static void methodA() throws Exception {
methodB();
}
如果:
methodB()
发生异常,而 methodA() 没有捕获,
就可能继续向:
methodA 的调用者
传播。
2.3 throws 写在哪里
完整方法结构:
修饰符 返回值类型 方法名(参数列表)
throws 异常类型 {
方法体
}
例如:
public static void parseDate()
throws java.text.ParseException {
}
所以不要写成:
throws public static void parseDate() {
}
也不要写进方法体:
public static void parseDate() {
throws ParseException;
}
throws 属于:
方法声明的一部分
2.4 throws 可以声明多个异常
例如:
public static void execute()
throws IOException, ParseException {
}
多个异常之间:
使用逗号分隔
表示:
当前方法可能向调用者传播这些异常。
2.5 Checked Exception 与 throws
对于 Checked Exception,Java 会进行编译期异常检查。
例如:
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class ThrowsDemo {
public static void main(String[] args)
throws ParseException {
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy/MM/dd");
sdf.parse("2026/09/02");
}
}
parse() 可能抛出:
ParseException
它属于 Checked Exception。
如果当前方法不准备:
try-catch
那么可以:
throws ParseException
继续向调用者声明传播。
因此对于 Checked Exception,调用者主要面临:
方案一:当前层捕获
try-catch
方案二:当前层不处理
throws
2.6 RuntimeException 是否必须写 throws
不必须。
例如:
public static int divide(int a, int b) {
return a / b;
}
这里可能产生:
ArithmeticException
但是:
public static int divide(int a, int b)
throws ArithmeticException {
return a / b;
}
虽然也可以写,
却不是编译器强制要求。
因为:
ArithmeticException
↓
RuntimeException
↓
Unchecked Exception
所以要建立一个非常重要的认识:
throws并不等于 Checked Exception 专属语法。
RuntimeException 也可以出现在 throws 声明中,只是通常不需要为了通过编译而声明。
2.7 throw 是什么
throw 和 throws 只有一个字母 s 的区别,但职责完全不同。
throw 表示:
主动抛出一个具体的异常对象。
基本语法:
throw 异常对象;
例如:
throw new IllegalArgumentException();
或者:
throw new IllegalArgumentException(
"年龄不能小于 0"
);
这里:
new IllegalArgumentException(...)
负责:
创建异常对象
而:
throw
负责:
把这个异常对象抛出去
2.8 为什么需要主动 throw
异常不一定只能由 JVM 或 JDK 自动产生。
例如:
public static void setAge(int age) {
if (age < 0 || age > 150) {
// 怎么处理?
}
}
从 Java 语法角度看:
age = -100
完全是一个合法整数。
JVM 不知道:
-100 岁
在你的业务中是不合法的。
因此程序员可以主动判断:
if (age < 0 || age > 150) {
throw new IllegalArgumentException(
"年龄必须在 0~150 之间"
);
}
此时:
Java 语言没有出错
JVM 也没有自动发现问题
↓
业务规则发现非法状态
↓
程序员主动创建异常
↓
throw 抛出异常
这就是 throw 的重要意义。
2.9 throw 与 throws 的核心区别
| 对比项 | throw | throws |
| ------------------------ | -------------------------- | --------------------- |
| 类型 | 语句 | 方法声明的一部分 |
| 位置 | 方法体内部 | 方法声明处 |
| 后面是什么 | 异常对象 | 异常类型 |
| 作用 | 真正抛出异常 | 声明可能传播异常 |
| 典型写法 | throw new XxxException() | throws XxxException |
| 是否真正产生异常传播动作 | 是 | 自身只是声明 |
最重要的一句话:
throw:我现在抛出一个异常对象
throws:我声明这个方法可能把异常交给调用者
2.10 throw 后面的代码会怎样
例如:
public static void checkAge(int age) {
if (age < 0) {
throw new IllegalArgumentException(
"年龄不能小于 0"
);
// System.out.println("继续执行");
}
}
一旦:
throw
执行,
当前正常执行路径就会被中断。
因此不能理解成:
throw 异常
↓
然后继续执行下一句
而应该理解成:
throw
↓
当前方法发生异常完成
↓
开始寻找能够处理异常的地方
2.11 什么叫异常传播
考虑三个方法:
public static void main(String[] args) {
methodA();
}
public static void methodA() {
methodB();
}
public static void methodB() {
int result = 10 / 0;
}
调用链:
main()
↓
methodA()
↓
methodB()
methodB() 中:
10 / 0
产生:
ArithmeticException
如果 methodB() 没处理:
methodB
↓
methodA
如果 methodA() 也没处理:
methodA
↓
main
如果 main() 依然没有处理:
main
↓
当前线程的默认未捕获异常处理流程
↓
线程异常终止
这就是异常沿:
方法调用栈
向上传播。
2.12 一个完整传播案例
public class PropagationDemo {
public static void main(String[] args) {
try {
level1();
System.out.println("success");
} catch (ArithmeticException e) {
System.out.println("failed");
}
System.out.println("end");
}
public static void level1() {
level2();
}
public static void level2() {
int result = 10 / 0;
System.out.println(result);
}
}
调用:
main
↓
level1
↓
level2
异常:
ArithmeticException
发生在:
level2
但 level2() 没有处理。
于是:
level2
↓
level1
level1() 也没有处理:
level1
↓
main
最终:
catch (ArithmeticException e)
能够接住。
因此执行:
failed
end
而:
success
不会输出。
三、使用方法
3.1 使用 throw 做参数校验
public class UserService {
public static void register(String username) {
if (username == null ||
username.isBlank()) {
throw new IllegalArgumentException(
"用户名不能为空"
);
}
System.out.println(
"注册用户:" + username
);
}
}
调用:
register("");
程序主动发现:
用户名不符合规则
于是抛出异常。
3.2 使用 throws 传播 Checked Exception
例如:
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class ParseService {
public static void parseDate(String text)
throws ParseException {
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd");
sdf.parse(text);
}
}
这里:
ParseService
不决定失败后应该怎么向用户反馈。
它只声明:
日期解析可能失败
由调用者决定处理策略。
3.3 最外层捕获
import java.text.ParseException;
public class App {
public static void main(String[] args) {
try {
ParseService.parseDate(
"2026-error"
);
} catch (ParseException e) {
System.out.println(
"请输入正确日期"
);
}
}
}
形成:
ParseService
↓
异常向上传播
↓
App / 边界层
↓
统一处理
3.4 不要为了省事全部 throws Exception
原始入门资料使用过:
public static void method()
throws Exception {
}
这有利于初学阶段理解:
异常可以继续向上传播
但是工程开发中不应该无脑这样写。
例如:
public User loadUser()
throws Exception {
}
调用者只能知道:
“可能发生某种 Exception”
却无法知道到底是:
文件异常
SQL 异常
解析异常
业务异常
更清晰的方式通常是:
throws IOException
或者:
throws ParseException
让方法的失败契约更加明确。
3.5 当前层到底捕获还是继续抛
可以问自己一个问题:
当前方法真的知道这个异常应该如何正确处理吗?
如果答案是:
知道
例如:
用户输入格式错误
→ 当前 UI 可以提示重新输入
那么可以:
当前层捕获
如果答案是:
不知道
例如底层 Repository:
只知道读取失败
不知道最终怎么回复用户
则更合理的方式可能是:
继续向上层传播
四、原理与进阶
4.1 异常传播本质上沿调用栈向上查找处理器
普通方法调用:
main
↓
A
↓
B
↓
C
当 C 正常完成:
C 返回
↑
B
↑
A
↑
main
如果 C 抛异常:
C
↑ 异常
B
↑ 异常
A
↑ 异常
main
Java 会沿调用关系寻找:
能够捕获该异常类型的 catch
找到以后:
进入 catch
找不到:
异常继续向上传播
4.2 throws 本身不会创建异常
这是非常容易混淆的问题。
例如:
public static void test()
throws IOException {
}
并不代表调用这个方法:
一定产生 IOException
throws 只是声明:
这个方法存在向外传播 IOException 的可能
真正抛异常的动作来自:
方法内部执行的代码
或者:
throw new IOException();
4.3 throw 的对象必须属于 Throwable 体系
官方补充:
throw 后面的表达式必须产生能够作为 Throwable 抛出的对象。
正确:
throw new RuntimeException();
错误思路:
// throw "error";
// throw 100;
因为:
String
int
都不是:
Throwable
体系中的异常对象。
4.4 Checked Exception 为什么影响方法签名
如果一个方法内部可能抛出 Checked Exception,又没有在方法内部捕获:
编译器必须知道
调用者需要面对这种失败可能
因此异常被写入:
throws XxxException
成为方法声明的一部分。
这意味着异常并不只是:
运行失败后的调试信息
也是:
API 契约的一部分
4.5 RuntimeException 为什么通常不要求 throws
例如:
public static void method(String value) {
value.length();
}
理论上可能:
NullPointerException
但如果所有可能的 RuntimeException 都强制写进方法签名:
throws NullPointerException,
IllegalArgumentException,
...
方法声明会极其臃肿。
因此 Java 不要求 unchecked exception 必须声明。
五、实践应用
5.1 三层调用模型
真实项目可以抽象为:
Controller
↓
Service
↓
Repository
Repository:
负责数据访问
Service:
负责业务逻辑
Controller:
负责系统边界与用户响应
如果底层发生问题:
Repository 异常
↓
Service
↓
Controller
↓
统一转换为用户可以理解的响应
这正是后续 Spring 项目中:
全局异常处理
的基础思想。
5.2 输入错误与程序错误不要混为一谈
例如:
Integer.parseInt(input);
用户输入:
abc
可能产生:
NumberFormatException
系统边界可以捕获并告诉用户:
请输入整数
但是:
User user = null;
user.getName();
如果是程序员本身的对象初始化错误,
单纯:
catch (NullPointerException e)
然后继续程序,
通常不是最好的解决方案。
应该优先修复:
为什么 user 会是 null
六、常见问题
6.1 throw 和 throws 有什么区别?
最简单的记忆方式:
throw
→ 抛对象
→ 方法体内
throws
→ 声明异常类型
→ 方法声明上
6.2 throws 会不会真正抛出异常?
throws 本身不会创建和抛出异常。
它负责声明:
异常可能继续传播
6.3 RuntimeException 能写 throws 吗?
可以。
但通常不是为了满足编译器要求。
6.4 Checked Exception 能不能不写 throws?
如果已经:
当前方法 try-catch 处理
则不需要继续声明。
如果没有捕获,并且这种 Checked Exception 可以从方法体传播出去,则必须符合 Java 的异常检查规则。
6.5 为什么不推荐到处 throws Exception?
因为范围过大,会削弱:
API 的异常语义
调用者的判断能力
编译器能够提供的帮助
6.6 throw 后还能继续执行下面代码吗?
当前执行路径不能像普通语句一样继续。
throw 会导致当前语句异常完成,并进入异常传播流程。
6.7 底层异常是不是全部应该抛到 main?
不是。
“向上抛”是处理策略之一,不是固定规则。
真正原则是:
在能够做出正确恢复、转换或响应决策的层级处理异常。
6.8 main 方法 throws Exception 好不好?
学习简单 Demo 时可以看到:
public static void main(String[] args)
throws Exception {
}
这能够快速让 Checked Exception 示例通过编译。
但它并没有真正设计:
异常发生以后应该怎么办
因此企业应用不能把它当成完整异常处理方案。
七、练习与验收
7.1 知识问答
throw与throws分别写在哪里?throw后面是什么?throws后面是什么?throws是否会真正创建异常对象?- 为什么需要主动
throw? - RuntimeException 是否必须声明
throws? - Checked Exception 为什么会影响方法声明?
- 一个方法可以声明多个异常吗?
- 什么叫异常传播?
- 异常通常沿什么结构向上传播?
- 如果所有调用层都不处理异常,最终可能发生什么?
- 为什么不推荐所有方法都
throws Exception? - 什么场景应该当前层处理异常?
- 什么场景更适合继续向上传播?
throw new IllegalArgumentException(...)完成了哪两个动作?
7.2 代码阅读
阅读:
public class ExceptionRead {
public static void main(String[] args) {
try {
show();
System.out.println("success");
} catch (Exception e) {
System.out.println("failed");
}
System.out.println("end");
}
public static void show() {
int x = 10 / 0;
System.out.println(x);
}
}
不运行回答:
show()中产生什么异常?show()是否自己处理异常?- 异常怎样到达
main()? success是否输出?failed是否输出?end是否输出?- 删除
try-catch后执行流程如何变化?
阅读:
public static void register(String username) {
if (username == null) {
throw new IllegalArgumentException(
"用户名不能为空"
);
}
System.out.println("register success");
}
回答:
throw后面是什么对象?- 为什么 Java 不会自动认为
username == null是当前业务异常? - 如果 username 为 null,最后一行是否执行?
- 这个异常是否属于 Checked Exception?
7.3 手写代码
任务一:参数校验
编写:
checkAge(int age)
规则:
0 ≤ age ≤ 150
否则主动抛出:
IllegalArgumentException
要求附带清晰异常消息。
任务二:日期解析传播
编写:
parseDate(String text)
调用日期解析 API。
要求:
- 当前方法不使用
try-catch。 - 使用准确的异常类型进行
throws声明。 - 在
main中捕获并处理。
任务三:三层调用链
设计:
main
↓
service
↓
repository
由 repository 主动制造一个异常。
要求:
- repository 不捕获。
- service 不捕获。
- main 统一捕获。
- 画出异常传播路径。
7.4 Debug
下面代码存在设计问题:
public static void register(String username)
throws Exception {
if (username == null ||
username.isBlank()) {
throw new Exception(
"用户名不合法"
);
}
}
分析:
throws Exception范围是否过大?new Exception表达的业务语义是否足够明确?- 调用者能够知道这是“用户名非法”问题吗?
- 下一章学习自定义异常以后应该如何优化?
7.5 综合训练
设计三层注册业务:
App
↓
UserService
↓
UserValidator
要求:
UserValidator 检查:
- 用户名不能为空。
- 用户名长度必须为 4~20。
- 年龄必须为 0~150。
当前阶段允许使用:
IllegalArgumentException
要求:
- 校验失败时使用
throw。 - Service 不负责最终用户提示。
- App 作为边界层集中处理。
- 画出正常调用流。
- 画出异常传播流。
- 分析下一章为什么值得引入自定义业务异常。
7.6 本章验收
- [ ] 能闭卷写出
throw new XxxException(...)。 - [ ] 能闭卷写出
throws XxxException。 - [ ] 能准确区分
throw和throws。 - [ ] 能解释 Checked Exception 与 RuntimeException 对
throws的影响。 - [ ] 能解释异常沿调用栈向上传播的过程。
- [ ] 能独立画出三层方法调用中的异常传播图。
- [ ] 能判断当前层应该捕获还是继续传播。
- [ ] 能发现
throws Exception过度宽泛的问题。 - [ ] 能使用主动抛异常表达参数或业务校验失败。