Java 异常与错误:从原理到实战的完整指南
前言
在 Java 开发中,异常处理是保证程序健壮性的核心机制之一。很多新手写代码时只会无脑 try-catch,却分不清 Error 和 Exception,也不知道受检 / 非受检异常的区别,甚至写出 “吞异常” 的反模式代码。
这篇博客会从底层原理、分类、语法到最佳实践,一次性讲透 Java 异常体系,帮你彻底告别 “异常处理全靠瞎写” 的困境。
一、Throwable:所有 “可抛出问题” 的顶层父类
Java 中所有错误和异常的祖宗类都是 java.lang.Throwable,只有继承了它的类,才能被 throw 抛出、被 catch 捕获。
它的核心子类只有两个:
Error:代表严重错误Exception:代表可处理的异常
java.lang.Throwable
├── java.lang.Error
└── java.lang.Exception
├── java.lang.RuntimeException (非受检异常)
└── 其他Exception (受检异常)
二、Error:程序无法挽回的致命问题
1. 什么是 Error?
Error 是 JVM 运行时出现的严重错误,这类问题和你的代码逻辑无关,是系统级别的故障,程序无法通过任何逻辑恢复,只能终止运行。
2. 核心特点
- 由 JVM 抛出,程序无需也无法捕获处理
- 一旦出现,通常意味着程序已经无法继续执行
- 不强制要求代码声明或捕获
3. 常见 Error 示例
表格
| 错误类型 | 场景说明 |
|---|---|
OutOfMemoryError |
堆内存溢出,对象太多导致 JVM 内存耗尽 |
StackOverflowError |
栈溢出,比如递归调用死循环、方法嵌套过深 |
VirtualMachineError |
JVM 本身故障,比如 JVM 崩溃 |
NoClassDefFoundError |
类定义找不到,通常是编译后类文件丢失 |
举个例子:写一个死递归,直接触发
StackOverflowErrorpublic class ErrorTest { public static void main(String[] args) { recursion(); } static void recursion() { recursion(); // 无限递归,栈帧不断压入,最终栈溢出 } }
三、Exception:程序可处理的异常
Exception 是程序运行时出现的、可以通过代码逻辑处理的问题,也是我们开发中重点关注的对象。它分为两大类:受检异常(Checked Exception) 和 非受检异常(Unchecked Exception)。
1. 受检异常(Checked Exception)
定义
除了 RuntimeException 及其子类之外的所有 Exception,编译器会强制要求你处理,不处理直接编译报错。
特点
- 由外部环境导致,属于 “合理预期内的异常”
- 必须显式处理:要么用
try-catch捕获,要么用throws声明抛出 - 代表代码无法控制的错误场景(比如文件不存在、网络断开)
常见示例
表格
| 异常类型 | 场景说明 |
|---|---|
IOException |
IO 操作异常(文件读写、网络传输) |
SQLException |
数据库操作异常 |
ClassNotFoundException |
类找不到,反射加载类失败 |
FileNotFoundException |
文件不存在,属于 IOException 的子类 |
比如你代码里的
FileInputStream就会抛出FileNotFoundException,属于受检异常,编译器强制要求你处理,否则编译不通过。
2. 非受检异常(Unchecked Exception)
定义
RuntimeException 及其子类,编译器不强制要求处理,不写 try-catch 也能编译通过,但运行时会直接抛出并终止程序。
特点
- 由代码逻辑错误导致,属于 “可以避免的错误”
- 不需要显式声明或捕获
- 代表程序员的疏忽或逻辑漏洞(比如空指针、下标越界)
常见示例
表格
| 异常类型 | 场景说明 |
|---|---|
NullPointerException |
空指针异常,调用了 null 对象的方法 / 属性 |
ArrayIndexOutOfBoundsException |
数组下标越界 |
ArithmeticException |
算术异常,比如除以 0 |
ClassCastException |
类型转换异常,比如把 String 强转为 Integer |
IllegalArgumentException |
参数非法,传入不符合要求的参数 |
举个例子:
public class RuntimeExceptionTest { public static void main(String[] args) { int[] arr = new int[3]; System.out.println(arr[5]); // 下标越界,运行时抛出 ArrayIndexOutOfBoundsException } }
四、Java 异常处理的核心语法
1. try-catch-finally:基础捕获结构
这是最基础的异常捕获方式,也是你代码里用到的结构。
语法格式
try {
// 可能抛出异常的代码块
} catch (异常类型1 e) {
// 捕获并处理异常类型1
} catch (异常类型2 e) {
// 捕获并处理异常类型2
} finally {
// 无论是否发生异常,都会执行的代码块(除了 System.exit(0))
}
关键要点
try:包裹可能出问题的代码,一旦出现异常,后续代码不会继续执行,直接跳转到catchcatch:捕获try中抛出的指定类型异常,多个catch要遵循 “子类在前,父类在后” 的顺序finally:用于资源释放,比如关闭 IO 流、数据库连接,是兜底操作
2. throws:声明抛出异常
当你不想在当前方法处理异常时,可以用 throws 声明,把异常抛给调用者处理。
语法示例
// 声明抛出 IOException,交给调用者处理
public static void readFile() throws IOException {
FileInputStream fis = new FileInputStream("demo.txt");
}
// 调用者可以选择 try-catch 处理,或者继续 throws 抛出
public static void main(String[] args) throws IOException {
readFile();
}
注意事项
- 子类重写父类方法时,不能抛出比父类更宽泛的异常(比如父类没抛异常,子类不能抛)
- 受检异常必须声明或捕获,非受检异常可以不写
3. throw:手动抛出异常
throw 用于手动抛出一个异常对象,通常用在业务逻辑校验中,主动抛出不符合业务规则的异常。
语法示例
public static void checkAge(int age) {
if (age < 0 || age > 150) {
// 手动抛出参数非法异常
throw new IllegalArgumentException("年龄必须在 0-150 之间,传入值:" + age);
}
}
public static void main(String[] args) {
checkAge(-5); // 运行时抛出 IllegalArgumentException
}
4. try-with-resources:自动关闭资源(Java 7+)
这是 Java 7 新增的语法,专门用来简化资源关闭操作,比如 IO 流、数据库连接,自动实现 finally 中的资源释放,比手动写 finally 更安全简洁。
适用场景
实现了 AutoCloseable 接口的资源(所有 IO 流、数据库连接池等都实现了这个接口)。
语法示例
// 传统写法:手动关闭流,容易忘写 finally 或出现空指针
FileInputStream fis = null;
try {
fis = new FileInputStream("demo.txt");
// 业务代码
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// try-with-resources 写法:自动关闭,无需写 finally
try (FileInputStream fis = new FileInputStream("demo.txt");
InputStreamReader isr = new InputStreamReader(fis, "UTF-8")) {
// 业务代码
} catch (IOException e) {
e.printStackTrace();
}
原理:离开
try块时,会自动按逆序调用资源的close()方法,无需手动处理。
五、异常处理的最佳实践(避坑指南)
1. 不要 “一刀切” 捕获所有异常
❌ 反模式:
try {
// 业务代码
} catch (Exception e) {
e.printStackTrace();
}
这种写法会掩盖所有异常,包括你没预料到的错误,后期排查问题极难。
✅ 正确做法:捕获具体的异常类型,按优先级从子类到父类排列。
try {
FileInputStream fis = new FileInputStream("demo.txt");
} catch (FileNotFoundException e) {
System.out.println("文件不存在,请检查路径");
} catch (IOException e) {
System.out.println("文件读取失败");
}
2. 不要 “吞异常”
❌ 反模式:
try {
// 业务代码
} catch (IOException e) {
// 空的 catch 块,什么都不做
}
捕获了异常却不处理、不打印日志,会导致问题被隐藏,程序出了问题完全无法定位。
✅ 正确做法:至少打印异常栈轨迹,或记录日志,带上上下文信息。
catch (IOException e) {
log.error("读取文件失败,路径:demo.txt", e); // 打印日志,带上上下文
e.printStackTrace(); // 开发环境打印栈轨迹
}
3. finally 里不要写 return
❌ 反模式:
public static int test() {
try {
return 1;
} finally {
return 2; // 会覆盖 try 里的 return,而且会吞掉异常
}
}
finally 里的 return 会覆盖 try/catch 里的返回值,同时会吞掉 try 中抛出的异常,导致程序行为不可预期。
✅ 正确做法:finally 只做资源释放,不写 return、break 等改变流程的语句。
4. 合理使用自定义异常
当业务场景有特殊的错误类型时,可以自定义异常,比如 BusinessException、OrderException,但不要滥用。
示例:
// 自定义业务异常
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
// 使用场景
public static void payOrder(int orderId) {
if (orderId <= 0) {
throw new BusinessException("订单号非法:" + orderId);
}
// 支付逻辑
}
5. 优先修复非受检异常,而不是捕获
非受检异常(比如 NullPointerException、ArrayIndexOutOfBoundsException)都是代码逻辑错误导致的,优先通过代码优化避免,而不是用 try-catch 捕获。
❌ 反模式:
try {
String str = null;
System.out.println(str.length());
} catch (NullPointerException e) {
// 捕获空指针,却不修复问题
}
✅ 正确做法:提前判空,避免空指针:
String str = null;
if (str != null) {
System.out.println(str.length());
}
六、总结
Throwable是所有错误和异常的顶层父类,分为Error和ExceptionError是 JVM 级别的严重错误,程序无法处理,只能终止Exception分为受检异常(编译强制处理)和非受检异常(代码逻辑错误)- 核心处理语法:
try-catch-finally、throws、throw、try-with-resources - 最佳实践:捕获具体异常、不吞异常、不滥用捕获、优先修复非受检异常
更多推荐



所有评论(0)