这篇指南将从以下几个方面展开,帮助你彻底理解 Java 异常:

- 什么是异常?
- 异常的体系结构
- 异常的分类(Checked vs Unchecked)
- 异常处理的关键字:
try,catch,finally,throw,throws - 如何正确使用异常(最佳实践)
- 自定义异常
什么是异常?
在 Java 中,异常指的是程序在运行过程中出现的非正常的、可被识别和处理的错误事件。
异常就是程序运行时发生的问题,这些问题中断了正常的指令流,如果没有异常处理机制,程序一旦遇到错误就会立即终止,这对于构建健壮、可靠的应用程序是不可接受的。
Java 的异常处理机制提供了一种将“正常流程代码”和“错误处理代码”分离的方式,使得代码更清晰、更易于维护。
异常的体系结构
所有 Java 异常的顶级父类都是 java.lang.Throwable 类,它有两个主要的子类:

Error(错误)Exception(异常)
下面是这个体系的简化结构图:
java.lang.Object
java.lang.Throwable
java.lang.Error
java.lang.Exception
java.lang.RuntimeException
... (其他非RuntimeException)
Error (错误)
- 性质:
Error类及其子类通常表示严重的、系统级的问题,这些问题通常是不可恢复的。 - 原因:它们不是由程序逻辑引起的,而是由 JVM(Java虚拟机)运行环境引起的。
OutOfMemoryError:内存溢出,JVM 没有足够内存分配。StackOverflowError:栈溢出,通常由无限递归引起。VirtualMachineError:虚拟机错误。
- 处理:对于
Error,我们通常无能为力,也无法通过try-catch来捕获和处理,程序一般会选择终止。
Exception (异常)
Exception 是我们日常编码中主要关注的部分,它表示程序本身可以处理的异常,根据是否在编译时被检查,Exception 又分为两大类:
异常的分类:Checked vs Unchecked
这是理解 Java 异常最核心的概念。
A. Checked Exception (受检异常 / 编译时异常)
- 定义:在编译时,Java 编译器会检查代码中是否处理了这类异常,如果没有,编译会失败。
- 目的:提醒开发者,这段代码可能会出问题,你必须提前考虑好应对策略。
- 特点:通常是外部环境引起的,例如文件不存在、网络连接中断、数据库访问失败等,这些错误是程序无法完全避免的。
- 示例:
IOException:输入/输出操作异常,如文件读写失败。SQLException:数据库访问错误。FileNotFoundException:文件未找到异常。ClassNotFoundException:类未找到异常。
如何处理 Checked Exception? 你必须显式地处理它,有两种方式:

try-catch:在可能抛出异常的代码块外包裹try-catch。try { FileReader file = new FileReader("a.txt"); } catch (FileNotFoundException e) { System.out.println("文件未找到,请检查路径!"); e.printStackTrace(); // 打印异常堆栈信息 }throws:在方法签名上声明,告诉调用者:“这个方法可能会抛出这个异常,处理的责任交给你了”。public void readFile() throws FileNotFoundException { FileReader file = new FileReader("a.txt"); // ... 读取文件 }
B. Unchecked Exception (非受检异常 / 运行时异常)
- 定义:在编译时,Java 编译器不会检查这类异常,它们在运行时才可能被发现。
- 目的:通常表示程序逻辑错误,这些错误本可以通过更好的代码设计来避免。
- 特点:它们是
RuntimeException类及其子类的实例,因为它们通常是编程错误,所以强制开发者去捕获每一个可能的逻辑错误是不现实的。 - 示例:
NullPointerException:空指针异常,试图访问一个null对象的成员。ArrayIndexOutOfBoundsException:数组下标越界。ClassCastException:类型转换异常。ArithmeticException:算术异常,如除以零。IllegalArgumentException:非法参数异常。
如何处理 Unchecked Exception? 你可以选择处理,但通常不推荐去捕获它们,而应该修复代码逻辑。
// 错误的示范:捕获了本应修复的逻辑错误
try {
String str = null;
System.out.println(str.length());
} catch (NullPointerException e) {
System.out.println("空指针了,没事,我catch一下");
} // 这是在掩盖bug,而不是修复它
// 正确的做法:修复代码
String str = "hello";
if (str != null) {
System.out.println(str.length());
}
在某些特定场景下,比如框架或工具类中,你可能需要捕获 RuntimeException 来进行统一的日志记录或资源清理。
异常处理的关键字
| 关键字 | 作用 |
|---|---|
try |
包裹可能抛出异常的代码块。 |
catch |
捕获并处理 try 块中抛出的特定异常,可以有多个 catch 块。 |
finally |
无论是否发生异常,finally 块中的代码一定会被执行,通常用于资源释放(如关闭文件、数据库连接)。 |
throw |
手动抛出一个异常对象,通常用在方法体内部。 |
throws |
在方法签名上声明该方法可能抛出的异常,将处理责任交给调用者。 |
代码示例:try-catch-finally
public class ExceptionDemo {
public static void main(String[] args) {
try {
System.out.println("进入 try 块");
int result = 10 / 0; // 抛出 ArithmeticException
System.out.println("这行代码不会执行");
} catch (ArithmeticException e) {
// 捕获到 ArithmeticException
System.out.println("捕获到异常: " + e.getMessage());
e.printStackTrace(); // 打印详细的异常堆栈,非常有助于调试
} finally {
// 无论是否发生异常,这里都会执行
System.out.println("进入 finally 块,执行资源清理工作...");
}
System.out.println("程序继续执行...");
}
}
输出:
进入 try 块
捕获到异常: / by zero
java.lang.ArithmeticException: / by zero
at ExceptionDemo.main(ExceptionDemo.java:6)
进入 finally 块,执行资源清理工作...
程序继续执行...
throw vs throws 的区别
| 特性 | throw |
throws |
|---|---|---|
| 位置 | 方法体内部 | 方法签名后面 |
| 作用 | 主动抛出一个异常对象 | 声明该方法可能抛出的异常 |
| 数量 | 一次只能抛出一个异常对象 | 可以声明多个异常,用逗号隔开 |
| 后续 | 抛出后,当前方法会立即终止,并将异常交给调用者处理 | 只是一个声明,方法本身不一定真的会抛出 |
示例:throw
public void checkAge(int age) {
if (age < 18) {
// 手动创建并抛出一个异常
throw new IllegalArgumentException("年龄必须大于18岁");
}
System.out.println("年龄合法");
}
示例:throws
public void readFile() throws IOException, FileNotFoundException {
// ... 读写文件,可能抛出这两种异常
// 这里不需要 try-catch,而是声明出去
}
// 调用方必须处理
public void callReadFile() {
try {
readFile();
} catch (IOException e) {
e.printStackTrace();
}
}
如何正确使用异常(最佳实践)
-
只对异常情况进行处理:不要用
try-catch来控制正常的业务流程。try块应该尽可能小,只包裹真正可能出错的代码。 -
优先捕获具体的异常:永远不要只捕获
Exception或Throwable,这会让你无法区分不同类型的错误,掩盖真正的问题。// 不好的做法 try { ... } catch (Exception e) { ... } // 好的做法 try { ... } catch (IOException e) { ... } catch (SQLException e) { ... } -
不要吞掉异常:不要只
catch一个异常然后什么都不做(比如只打印一个日志),这会让问题在底层静默消失,导致上层调用者无法感知,从而引发更严重的错误。// 不好的做法:吞掉异常 try { ... } catch (IOException e) { System.out.println("出错了"); } -
在
finally块中释放资源:确保数据库连接、文件流、网络连接等资源能够被正确关闭,避免资源泄漏。 -
记录异常信息:在
catch块中,使用logger.error(..., e)或e.printStackTrace()记录完整的异常堆栈信息,这对于后续的调试至关重要。 -
优先使用标准异常:Java 标准库中已有合适的异常(如
IllegalArgumentException,IllegalStateException),直接使用它们,而不是创建新的自定义异常。 -
保持异常的原始信息:在捕获一个异常后重新抛出另一个异常时,最好将原始异常作为“原因”传递进去,这样可以保留完整的调用链。
try { // ... 一些操作 } catch (SQLException e) { // 将原始异常作为原因包装后重新抛出 throw new BusinessException("数据库操作失败", e); }
自定义异常
当你需要表达特定的业务错误,并且希望这个错误有明确的业务含义时,可以创建自定义异常。
步骤:
- 继承
Exception(对于受检异常)或RuntimeException(对于非受检异常)。 - 可以选择性地重写构造方法,以便可以传递错误消息和原始异常。
示例:自定义业务异常
// 1. 自定义一个非受检的业务异常
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
// 2. 在业务代码中使用它
public class UserService {
public void register(String username, String password) {
if (username == null || username.trim().isEmpty()) {
// 抛出自定义异常
throw new BusinessException("用户名不能为空");
}
if (password.length() < 6) {
throw new BusinessException("密码长度不能少于6位");
}
// ... 注册逻辑
}
}
// 3. 调用方处理
public class Main {
public static void main(String[] args) {
UserService userService = new UserService();
try {
userService.register("", "123456");
} catch (BusinessException e) {
System.out.println("注册失败: " + e.getMessage());
// e.printStackTrace(); // 可以打印堆栈
}
}
}
| 特性 | Error |
Checked Exception |
Unchecked Exception |
|---|---|---|---|
| 父类 | Error |
Exception (非 RuntimeException) |
RuntimeException |
| 处理方式 | 不处理,程序终止 | 必须处理 (try-catch 或 throws) |
不强制处理,建议修复代码 |
| 发生时机 | 运行时 | 编译时检查 | 运行时 |
| 常见原因 | JVM系统问题 | 外部环境问题(IO、DB等) | 程序逻辑错误(NPE、越界等) |
掌握 Java 异常是编写高质量、健壮代码的必备技能,理解其分类、处理机制并遵循最佳实践,能让你写出更专业、更易维护的程序。
