1. 引言

本文将深入解析 Java 中两个关键异常处理关键字:throwthrows。它们虽然只差一个字母,但用途完全不同,稍不注意就容易踩坑。

我们将通过实际代码示例说明它们的使用场景,并总结一些生产环境中的最佳实践。目标是让你在面对异常设计时,能做出更合理的选择。

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

📌 技术细节:所有 RuntimeExceptionError 的子类都是非受检异常,其余 Throwable 子类为受检异常。

5. 总结

  • throw 用于抛出异常实例,在方法内部使用
  • throws 用于声明异常类型,在方法签名中使用
  • ✅ 优先使用具体异常,必要时定义自定义异常
  • ✅ 合理使用异常包装,隔离底层细节
  • ✅ 区分受检与非受检异常,理解编译器的强制要求

掌握这些知识点,能让你在设计 API 时更合理地暴露异常,避免调用方“一脸懵”。

源码示例已上传至 GitHub:https://github.com/tech-tutorial/core-java-exceptions
想深入了解 Java 异常体系,推荐阅读我们的专题文章《Java 异常处理全解析》


原始标题:Difference Between Throw and Throws in Java