throw与throws

throw:手动抛异常

就是你在代码里发现了不对劲的情况,主动”报警”

比喻:你是餐厅服务员,发现菜里有虫子,你主动喊”有问题!”——这就是throw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testThrow() {
System.out.println("=== throw 手动抛异常 ===");

try {
setAge(-5); // 传了一个不合理的值
} catch (IllegalArgumentException e) {
System.out.println("捕获到异常:" + e.getMessage());
// 输出:捕获到异常:年龄不能为负数,你输入的是:-5
}
}

// 在方法内部检查参数,不对就throw
public void setAge(int age) {
if (age < 0) {
// throw后面跟一个异常对象,用new创建
throw new IllegalArgumentException("年龄不能为负数,你输入的是:" + age);
}
System.out.println("年龄设置成功:" + age);
}

throw的语法throw new 异常类名("异常描述信息");

throw后面的代码不会执行,因为抛异常就相当于”这个方法到此为止了”

throws:声明异常

写在方法签名上,告诉调用者”我这个方法可能会出事,你自己小心”

比喻:快递包裹上写”易碎品”——不是说它一定会碎,而是提醒你小心处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testThrows() {
System.out.println("=== throws 声明异常 ===");

try {
readFile("not_exist.txt");
} catch (IOException e) {
System.out.println("调用者处理异常:" + e.getMessage());
}
}

// throws 写在方法签名上,声明这个方法可能抛出IOException
// 调用者必须处理(try-catch 或继续 throws)
public void readFile(String path) throws IOException {
FileInputStream fis = new FileInputStream(path); // 可能文件不存在
fis.read();
fis.close();
}

// 可以声明多个异常,用逗号隔开
public void riskyMethod() throws IOException, SQLException {
// ...
}

关键点

throws是给受检异常(Checked Exception)用的

非受检异常(RuntimeException及其子类)不需要throws声明(但你写了也不报错)

throws只是”声明”,不是”处理”,真正的处理还得靠 try-catch-finally

throw vs throws 对比表

对比项 throw throws
位置 方法体内 方法签名上
作用 手动抛出一个异常对象 声明方法可能抛出的异常
后面跟什么 异常对象new XxxException(...)) 异常类名(可以多个,逗号分隔)
数量 一次只能throw一个 可以声明多个
谁来处理 当前方法的调用者 当前方法的调用者
语法示例 throw new IOException("出错了"); void read() throws IOException
必须用吗 主动抛异常时用 受检异常不处理时必须声明

异常传播链

异常就像烫手山芋,一层一层往上抛,直到有人接住为止

1
2
3
4
5
6
7
8
9
methodC() 抛出异常
↓ methodC没有try-catch
methodB() 接到异常,也没处理
↓ methodB也没有try-catch
methodA() 接到异常,也没处理
↓ methodA也没有try-catch
main() 接到异常,还是没处理

JVM收到异常 → 打印堆栈信息 → 程序崩溃
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void testExceptionChain() {
System.out.println("=== 异常传播链 ===");
try {
methodA();
} catch (Exception e) {
System.out.println("最终在这里被接住了");
e.printStackTrace();
// 堆栈信息会清楚地显示:异常从methodC→methodB→methodA→这里
}
}

public void methodA() throws Exception {
System.out.println("进入methodA");
methodB(); // 调用B,B的异常会传到这里
}

public void methodB() throws Exception {
System.out.println("进入methodB");
methodC(); // 调用C,C的异常会传到这里
}

public void methodC() throws Exception {
System.out.println("进入methodC");
throw new Exception("methodC出事了!"); // 异常从这里开始
}

堆栈信息(Stack Trace)

出异常后打印的那一坨信息,从下往上读就是异常传播的路径

最上面一行是异常发生的地方,往下是一层层的调用者

看到异常先看第一行,那就是出问题的代码位置

实际开发中的套路

套路1:底层throw,上层catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Service层:发现问题就throw
public User findUser(Long id) {
User user = userDao.findById(id);
if (user == null) {
throw new RuntimeException("用户不存在,ID:" + id);
}
return user;
}

// Controller层:统一catch处理
@GetMapping("/user/{id}")
public Result getUser(@PathVariable Long id) {
try {
User user = userService.findUser(id);
return Result.success(user);
} catch (RuntimeException e) {
return Result.fail(e.getMessage());
}
}

套路2:异常转换(包装异常)

底层抛的异常太底层了(比如SQLException),上层不想知道这些细节

把底层异常包装成业务异常,再往上抛

1
2
3
4
5
6
7
8
9
public void saveUser(User user) {
try {
userDao.insert(user);
} catch (SQLException e) {
// 把底层异常包装成业务异常
// 第二个参数传原始异常,保留堆栈信息(很重要!)
throw new RuntimeException("保存用户失败", e);
}
}

更好的做法是用 自定义异常 代替 RuntimeException

常见坑

坑1:throws声明了异常但调用者不处理

如果是受检异常,编译器会报错,强制你处理

如果是非受检异常,编译器不管,但运行时会崩——所以别掉以轻心

坑2:throw后面还写代码

1
2
throw new RuntimeException("出错了");
System.out.println("这行永远执行不到"); // ❌ 编译报错:unreachable statement

坑3:catch了异常又原样throw,但丢了原始信息

1
2
3
4
5
6
7
8
9
// ❌ 错误:丢了原始异常的堆栈信息
catch (SQLException e) {
throw new RuntimeException("操作失败"); // 原始异常e丢了
}

// ✅ 正确:把原始异常作为cause传进去
catch (SQLException e) {
throw new RuntimeException("操作失败", e); // 保留了原始信息
}

坑4:在循环里throw异常来控制流程

异常的创建和抛出开销很大,别用它代替break/continue

练习题

题1:throw和throws的区别是什么?各写一行示例

throw:方法体内,手动抛出异常对象。throw new IllegalArgumentException("参数错误");

throws:方法签名上,声明可能的异常。public void read() throws IOException {}

题2:以下代码有什么问题?

1
2
3
public void divide(int a, int b) throws ArithmeticException {
System.out.println(a / b);
}

答案:ArithmeticException 是RuntimeException的子类(非受检异常),写throws没有实际意义,不会强制调用者处理。更好的做法是在方法内用if判断 b == 0 然后throw一个有意义的异常信息

题3:写一个方法,接收字符串参数,如果是null或空字符串就抛异常

1
2
3
4
5
6
public void validateName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("名字不能为空");
}
System.out.println("名字合法:" + name);
}

相关笔记

异常体系 | try-catch-finally | 自定义异常


上一章 目录 下一章
try-catch-finally java基础 自定义异常