首页 / Java 入门教程 / 抛出异常

Java 入门教程

抛出异常

本教程共 100 篇 · 第 68 篇 · 更新于 2026-08-05 · 约 18 分钟阅读

JavaJava 入门教程throwthrows异常传播

本节目标:分清 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 对比

两个词只差一个字母,作用完全不同。这是初学者最容易混的点之一。

throwthrows
是什么语句方法签名的修饰部分
写在哪方法体内部方法名和参数表之后
后面跟一个异常对象一个或多个异常类名
什么时候起作用运行时,立刻抛出编译期,约束调用方
数量一次抛一个可声明多个

一句话记: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

语法上也允许给 RuntimeExceptionthrows,比如 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)

调用栈完整记录了传播路径。从下往上读mainlevel1level1level2level2level3,异常在 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 已有的异常类型。IllegalArgumentExceptionIllegalStateExceptionUnsupportedOperationException 这几个能覆盖绝大多数场景,别一上来就自己造。

用 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 打印栈信息并终止线程。

重写方法时,子类的受检异常范围不能超过父类;非受检异常不受此限制。