抛出异常
本教程共 100 篇 · 第 68 篇 · 更新于 2026-08-05 · 约 18 分钟阅读
本节目标:分清 throw 和 throws 的区别,学会主动抛出异常和在方法签名上声明异常,理解异常沿调用栈向上传播的完整过程。
前面只学了「接」,现在学「发」
到目前为止我们都在接异常——try...catch 把别人抛来的异常接住。
但异常总得有人抛。Integer.parseInt("abc") 抛的 NumberFormatException,就是 JDK 源码里主动抛出来的。
轮到你写方法时,发现参数不对、状态不合法,也该主动抛异常告诉调用方「这样不行」。
throw:现在就抛
throw 是一条语句,作用是立刻抛出一个异常对象。
分两步走:先创建异常实例,再用 throw 扔出去。
public class ThrowDemo {
public static void main(String[] args) {
try {
setAge(-5);
} catch (IllegalArgumentException e) {
System.out.println("捕获:" + e.getMessage());
}
}
static void setAge(int age) {
if (age < 0 || age > 150) {
// 创建实例并抛出,通常合并成一行
throw new IllegalArgumentException("年龄必须在 0-150 之间,收到:" + age);
}
System.out.println("年龄设置为 " + age);
}
}
输出:捕获:年龄必须在 0-150 之间,收到:-5
throw 后面必须跟一个 Throwable 类型的对象。抛 String 或者数字都不行,编译不过。
throw 执行后,当前方法立即终止,后面的代码一行都不会跑。这一点和 return 很像。
static void demo() {
System.out.println("A");
throw new RuntimeException("停");
// System.out.println("B"); // 编译报错:unreachable statement
}
编译器能识别出 throw 之后的代码永远执行不到,直接报「无法访问的语句」。
异常消息要写得有用
创建异常时传的那个字符串,最终会出现在日志和调用栈里。写得好不好,直接影响排查效率。
// 没用的消息
throw new IllegalArgumentException("参数错误");
// 有用的消息:说清哪个参数、什么值、期望什么
throw new IllegalArgumentException("orderId 不能为空");
throw new IllegalArgumentException("折扣率超出范围:" + rate + ",应在 0.0-1.0 之间");
Tip一个好的异常消息应该让人不看代码就知道哪里错了。把出错的实际值带上,比任何形容词都管用。
throws:声明我可能会抛
throws 是方法签名的一部分,写在参数列表后面,用来告诉调用方「调我可能会出这些异常」。
import java.io.IOException;
public class ThrowsDemo {
// 声明本方法可能抛出 IOException
static void readConfig(String path) throws IOException {
if (path == null) {
throw new IOException("配置路径为空");
}
System.out.println("读取 " + path);
}
public static void main(String[] args) {
try {
readConfig(null);
} catch (IOException e) {
System.out.println("处理:" + e.getMessage());
}
}
}
一次可以声明多个,逗号隔开:
static void process() throws IOException, InterruptedException {
// ...
}
throw 和 throws 对比
两个词只差一个字母,作用完全不同。这是初学者最容易混的点之一。
throw | throws | |
|---|---|---|
| 是什么 | 语句 | 方法签名的修饰部分 |
| 写在哪 | 方法体内部 | 方法名和参数表之后 |
| 后面跟 | 一个异常对象 | 一个或多个异常类名 |
| 什么时候起作用 | 运行时,立刻抛出 | 编译期,约束调用方 |
| 数量 | 一次抛一个 | 可声明多个 |
一句话记:throw 是动作,throws 是声明。
// throws 是声明:我可能会抛
static void demo(int n) throws IOException {
if (n < 0) {
throw new IOException("负数不行"); // throw 是动作:现在真抛了
}
}
什么时候必须写 throws
规则跟第 65 章讲的受检异常一脉相承。
方法内部抛出受检异常(非 RuntimeException 分支)且不自己捕获,就必须在签名上用 throws 声明。否则编译不通过。
// 编译报错:unreported exception IOException
static void bad() {
throw new IOException("出错");
}
// 正确:声明出去
static void good() throws IOException {
throw new IOException("出错");
}
// 也正确:自己捕获掉
static void alsoGood() {
try {
throw new IOException("出错");
} catch (IOException e) {
System.out.println("自己处理了");
}
}
抛出非受检异常(RuntimeException 分支)则完全不需要声明:
// 不用写 throws,编译照过
static void checkAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负");
}
}
Note语法上也允许给
RuntimeException写throws,比如throws IllegalArgumentException。这不影响编译,但能起到文档作用,提醒调用方注意。实际项目里更常用 Javadoc 的@throws标签来做这件事。
异常沿调用栈向上传播
一个方法抛出异常后,如果自己没接住,异常会逐层往调用它的方法传,直到遇到某个 catch。
public class PropagationDemo {
public static void main(String[] args) {
try {
level1();
} catch (RuntimeException e) {
System.out.println("在 main 接住了:" + e.getMessage());
e.printStackTrace();
}
}
static void level1() {
level2(); // 不捕获,异常穿过去
}
static void level2() {
level3(); // 不捕获,异常穿过去
}
static void level3() {
throw new RuntimeException("最底层出错");
}
}
输出:
在 main 接住了:最底层出错
java.lang.RuntimeException: 最底层出错
at PropagationDemo.level3(PropagationDemo.java:19)
at PropagationDemo.level2(PropagationDemo.java:15)
at PropagationDemo.level1(PropagationDemo.java:11)
at PropagationDemo.main(PropagationDemo.java:4)
调用栈完整记录了传播路径。从下往上读:main 调 level1,level1 调 level2,level2 调 level3,异常在 level3 第 19 行诞生。
排查线上问题时,这份栈信息就是全部线索来源。
没人接会怎样
一路传到 main 还是没人接,JVM 会接管:打印异常栈到标准错误流,然后终止当前线程。
public class UncaughtDemo {
public static void main(String[] args) {
System.out.println("开始");
throw new IllegalStateException("没人管我");
// 后面的代码不会执行
}
}
控制台输出:
开始
Exception in thread "main" java.lang.IllegalStateException: 没人管我
at UncaughtDemo.main(UncaughtDemo.java:4)
main 方法也可以声明 throws Exception,把所有异常交给 JVM:
public class MainThrows {
// 写测试代码时的偷懒写法
public static void main(String[] args) throws Exception {
String content = java.nio.file.Files.readString(
java.nio.file.Path.of("demo.txt"));
System.out.println(content);
}
}
省掉了 try...catch,代价是一出错程序直接退出。写临时脚本或练习代码时可以,正式代码不要这么干。
重写方法时的异常约束
子类重写父类方法时,throws 声明不能随便改。规则是:子类不能抛出比父类更宽泛的受检异常。
import java.io.FileNotFoundException;
import java.io.IOException;
class Parent {
void doWork() throws IOException {
System.out.println("父类实现");
}
}
class Child1 extends Parent {
@Override
void doWork() throws FileNotFoundException { // 子类,范围更小,允许
System.out.println("子类实现");
}
}
class Child2 extends Parent {
@Override
void doWork() { // 一个都不抛,允许
System.out.println("子类实现");
}
}
反过来就不行:
class BadChild extends Parent {
@Override
void doWork() throws Exception { // 编译报错:范围变大了
}
}
道理很实在。多态调用时,调用方拿的可能是 Parent 类型的引用,它只准备了处理 IOException 的代码。子类要是偷偷抛出更宽泛的异常,调用方就接不住了。
这条约束只管受检异常。非受检异常想抛多少抛多少,不受限制。
什么时候该主动抛异常
三种典型场景。
参数校验不通过。 方法开头检查入参,不合法就抛 IllegalArgumentException。
static double divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("除数不能为 0");
}
return (double) a / b;
}
对象状态不允许当前操作。 抛 IllegalStateException。
class Connection {
private boolean open = false;
void send(String msg) {
if (!open) {
throw new IllegalStateException("连接未打开,不能发送数据");
}
System.out.println("发送:" + msg);
}
}
遇到不该出现的情况。 比如 switch 走到了理论上不可能的分支。
static String describe(int level) {
return switch (level) {
case 1 -> "低";
case 2 -> "中";
case 3 -> "高";
default -> throw new IllegalArgumentException("未知级别:" + level);
};
}
Tip优先复用 JDK 已有的异常类型。
IllegalArgumentException、IllegalStateException、UnsupportedOperationException这几个能覆盖绝大多数场景,别一上来就自己造。
用 Objects 简化参数校验
java.util.Objects 里有几个现成的校验方法,比手写 if 更简洁。
import java.util.Objects;
public class ObjectsCheck {
static void save(String name, int index, int size) {
// null 检查:为 null 就抛 NPE,消息用第二个参数
Objects.requireNonNull(name, "name 不能为 null");
// 索引检查:越界抛 IndexOutOfBoundsException
Objects.checkIndex(index, size);
System.out.println("保存 " + name);
}
public static void main(String[] args) {
try {
save(null, 0, 5);
} catch (NullPointerException e) {
System.out.println("捕获:" + e.getMessage());
}
}
}
输出:捕获:name 不能为 null
requireNonNull 还有一个返回值版本,可以直接用在赋值语句里:
public class Order {
private final String id;
public Order(String id) {
// 校验 + 赋值一步到位
this.id = Objects.requireNonNull(id, "订单号不能为空");
}
}
别在 finally 里抛异常
第 67 章讲过 finally 会覆盖返回值,抛异常同理,而且后果更严重。
// 反面教材
static void bad() {
try {
throw new IllegalArgumentException("真正的错误");
} finally {
throw new IllegalStateException("finally 的错误"); // 把上面那个吞了
}
}
调用方只能看到 IllegalStateException,真正的问题被彻底掩盖。
finally 里如果调用了可能抛异常的方法,要么自己捕获掉,要么改用 try-with-resources。
throw null 会发生什么
一个容易被忽略的边界情况:
public class ThrowNull {
public static void main(String[] args) {
Exception e = null;
try {
throw e; // 编译能过
} catch (Exception ex) {
System.out.println(ex.getClass().getSimpleName());
}
}
}
输出是 NullPointerException。
JVM 发现要抛的对象是 null,就自动抛出一个 NullPointerException 顶替。这个行为不算错,但会让人一头雾水——日志里的 NPE 跟你写的 throw 语句对不上号。
抛异常前确保对象不是 null,尤其是从方法返回值里拿异常对象的时候。
小结
throw 是语句,写在方法体里,抛出一个具体的异常对象,执行后方法立即终止。
throws 是签名的一部分,写在参数表后面,声明方法可能抛出哪些异常,主要约束受检异常。
异常没被捕获就沿调用栈逐层向上传,一直传到 main 还没人接,JVM 打印栈信息并终止线程。
重写方法时,子类的受检异常范围不能超过父类;非受检异常不受此限制。