【JavaSE全面教学】Java异常处理机制Day11(2026年)
写在前面:这是JavaSE系列的第11篇,进入进阶特性阶段。异常处理是写出健壮代码的必备技能,面试也经常问。很多人写代码只管happy path,不考虑异常情况,结果上线后各种崩溃。今天把Java的异常体系彻底讲清楚。

一、什么是异常?
1.1 异常的概念
异常:程序在运行过程中出现的不正常情况。
踩坑提醒:新手最容易犯的错误就是忽视异常处理。曾经有一个线上Bug让我记忆犹新——用户上传文件时,因为磁盘满了导致IOException,但代码里没有处理,整个服务直接崩溃,影响了所有用户。
// 常见的异常场景
int a = 10 / 0; // ArithmeticException
int[] arr = new int[3];
arr[5] = 10; // ArrayIndexOutOfBoundsException
String s = null;
s.length(); // NullPointerException
int num = Integer.parseInt("abc"); // NumberFormatException
异常的本质:Java用对象来表示异常,每个异常都是一个类的实例。
1.2 为什么要处理异常?
不处理异常的后果:
1. 程序直接崩溃,用户体验极差
2. 数据可能丢失或损坏
3. 安全漏洞(异常信息暴露系统细节)
4. 难以定位和修复问题
处理异常的好处:
1. 程序不会意外终止
2. 给用户友好的错误提示
3. 便于记录日志和排查问题
4. 保证资源的正确释放
二、Java异常体系
2.1 异常类层次结构
Throwable(所有异常的根类)
├── Error(错误,程序无法处理)
│ ├── OutOfMemoryError // 内存溢出
│ ├── StackOverflowError // 栈溢出
│ └── NoClassDefFoundError // 类找不到
│
└── Exception(异常,程序可以处理)
├── RuntimeException(运行时异常,也叫不受检异常)
│ ├── NullPointerException // 空指针
│ ├── ArrayIndexOutOfBoundsException // 数组越界
│ ├── ArithmeticException // 算术错误
│ ├── ClassCastException // 类型转换错误
│ ├── NumberFormatException // 数字格式错误
│ └── IllegalArgumentException // 非法参数
│
└── 非RuntimeException(编译时异常,也叫受检异常)
├── IOException // IO异常
│ ├── FileNotFoundException // 文件找不到
│ └── EOFException // 文件结束
├── SQLException // 数据库异常
└── ClassNotFoundException // 类找不到
2.2 Error vs Exception
| 类型 | 说明 | 能否处理 |
|---|---|---|
| Error | JVM级别的严重错误 | 不能(程序直接退出) |
| Exception | 程序级别的异常 | 能(try-catch处理) |
// Error:不需要也不应该处理
// OutOfMemoryError → 增加JVM内存,优化代码
// StackOverflowError → 检查递归是否正确
// Exception:必须或应该处理
// IOException → try-catch或throws
// NullPointerException → 代码中加null检查
2.3 受检异常 vs 不受检异常
| 类型 | 说明 | 编译器检查 | 处理方式 |
|---|---|---|---|
| 受检异常 | 非RuntimeException | ✅ 强制检查 | 必须try-catch或throws |
| 不受检异常 | RuntimeException | ❌ 不检查 | 可以不处理(但建议处理) |
// 受检异常:必须处理
public void readFile() throws IOException { // 必须声明throws
FileInputStream fis = new FileInputStream("test.txt"); // 编译错误!
}
// 不受检异常:可以不处理
public void divide() {
int a = 10 / 0; // 编译通过,运行时才报错
}
三、异常处理的五种方式
3.1 try-catch(捕获异常)
try {
// 可能出现异常的代码
int a = 10 / 0;
} catch (ArithmeticException e) {
// 捕获特定异常
System.out.println("算术异常:" + e.getMessage());
} catch (Exception e) {
// 捕获其他异常
System.out.println("未知异常:" + e.getMessage());
}
多个catch的顺序:
try {
// ...
} catch (NullPointerException e) {
// 先捕获子类异常
System.out.println("空指针异常");
} catch (RuntimeException e) {
// 再捕获父类异常
System.out.println("运行时异常");
} catch (Exception e) {
// 最后捕获最大的异常
System.out.println("异常");
}
// ❌ 错误:先捕获父类,子类永远捕获不到
// catch (Exception e) { }
// catch (NullPointerException e) { } // 编译错误!
3.2 try-catch-finally
经验之谈:finally块是资源释放的保险,无论try中发生什么(除了System.exit),finally都会执行。这是Java保证资源不泄露的机制。
try {
// 可能出现异常的代码
FileInputStream fis = new FileInputStream("test.txt");
// 使用fis...
} catch (IOException e) {
// 处理异常
System.out.println("IO异常:" + e.getMessage());
} finally {
// 无论是否异常都会执行
// 通常用于关闭资源
System.out.println("finally块执行了");
}
finally的执行时机:
public static int test() {
try {
System.out.println("try");
return 1;
} catch (Exception e) {
System.out.println("catch");
return 2;
} finally {
System.out.println("finally"); // 一定会执行
// return 3; // 不建议在finally中return
}
}
test(); // 输出:try → finally → 返回1
finally不执行的情况:
// 1. 在try或catch中调用了System.exit()
try {
System.exit(0); // 直接退出JVM
} finally {
System.out.println("不会执行"); // 不会执行
}
// 2. 程序所在线程死亡
// 3. 关闭CPU
3.3 try-finally(不捕获,只释放资源)
// 没有catch,异常会继续向上抛出
// 但finally中的资源释放代码一定会执行
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
int data = fis.read();
} finally {
if (fis != null) {
try {
fis.close(); // 关闭资源也可能抛异常
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.4 throws(声明异常)
踩坑提醒:throws不是把异常抛出去就完事了,最终一定要在某个地方捕获处理。如果一路throws到main方法,那就是把锅甩给了JVM,程序会直接崩溃。
// 方法声明throws,表示这个方法可能抛出异常
// 调用者必须处理
public void readFile(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
fis.close();
}
// 调用者处理方式1:try-catch
public void caller1() {
try {
readFile("test.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
// 调用者处理方式2:继续throws
public void caller2() throws IOException {
readFile("test.txt");
}
经验之谈:在分层架构中,DAO层抛出SQLException,Service层转换为业务异常,Controller层统一处理返回给前端。这种分层处理异常的方式让代码更清晰。
3.5 throw(手动抛出异常)
// throw:在方法内部手动抛出异常
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合法:" + age);
}
this.age = age;
}
// throws:在方法签名上声明
public void setAge(int age) throws IllegalArgumentException {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合法:" + age);
}
this.age = age;
}
四、自定义异常
4.1 为什么需要自定义异常?
// Java内置的异常太笼统
throw new Exception("余额不足"); // 不够具体
// 自定义异常更语义化
throw new InsufficientBalanceException("余额不足,当前余额:" + balance);
4.2 自定义异常的写法
// 自定义运行时异常(不受检)
class InsufficientBalanceException extends RuntimeException {
public InsufficientBalanceException(String message) {
super(message);
}
public InsufficientBalanceException(String message, Throwable cause) {
super(message, cause);
}
}
// 自定义受检异常
class BusinessException extends Exception {
private int code; // 错误码
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
4.3 使用自定义异常
class BankAccount {
private double balance;
public void withdraw(double amount) throws BusinessException {
if (amount <= 0) {
throw new BusinessException(1001, "取款金额必须大于0");
}
if (amount > balance) {
throw new BusinessException(1002, "余额不足,当前余额:" + balance);
}
balance -= amount;
}
}
public class Test {
public static void main(String[] args) {
BankAccount account = new BankAccount();
try {
account.withdraw(-100);
} catch (BusinessException e) {
System.out.println("错误码:" + e.getCode());
System.out.println("错误信息:" + e.getMessage());
}
}
}
五、异常处理的最佳实践
5.1 try-with-resources(Java 7+,推荐)
为什么推荐try-with-resources?
在Java 7之前,关闭资源需要写一大堆finally代码,既繁琐又容易出错。try-with-resources让资源管理变得优雅,而且异常处理也更完善——如果try和close都抛异常,close的异常会被抑制,不会丢失原始异常信息。
// 旧写法:手动关闭资源
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
int data = fis.read();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 新写法:自动关闭资源(推荐)
try (FileInputStream fis = new FileInputStream("test.txt")) {
int data = fis.read();
} catch (IOException e) {
e.printStackTrace();
}
// fis会自动关闭,不需要finally
// 多个资源
try (
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")
) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
// 两个流都会自动关闭
踩坑提醒:try-with-resources要求资源必须实现AutoCloseable接口。自定义资源时记得实现这个接口。
5.2 异常处理的规范
经验之谈:生产环境绝对不能直接用e.printStackTrace()!它会输出到控制台而不是日志文件,一旦服务重启,异常信息就丢失了。正确的做法是使用日志框架(如SLF4J)记录。
// ✅ 正确做法
public void method() {
try {
riskyOperation();
} catch (SpecificException e) {
log.error("具体异常", e); // 记录日志
throw new BusinessException("用户友好的提示"); // 抛出业务异常
}
}
// ❌ 错误做法
public void method() {
try {
riskyOperation();
} catch (Exception e) {
e.printStackTrace(); // 只打印到控制台,生产环境不推荐
}
}
// ❌ 最恶劣:吞掉异常
public void method() {
try {
riskyOperation();
} catch (Exception e) {
// 什么都不做,异常被"吞掉"了
}
}
踩坑提醒:吞掉异常是调试噩梦!曾经排查一个Bug花了整整一天,最后发现是某个工具类把异常吞掉了,导致上层完全不知道发生了什么。
5.3 异常链
public void readFile() throws BusinessException {
try {
FileInputStream fis = new FileInputStream("config.txt");
} catch (FileNotFoundException e) {
// 保留原始异常信息
throw new BusinessException(5001, "配置文件加载失败", e);
}
}
// 获取异常链
try {
readFile();
} catch (BusinessException e) {
System.out.println("业务异常:" + e.getMessage());
System.out.println("原始异常:" + e.getCause().getMessage());
e.printStackTrace(); // 打印完整异常链
}
六、面试高频考点
考点1:final、finally、finalize的区别
| 关键字 | 作用 |
|---|---|
final |
修饰类/方法/变量,表示不可变 |
finally |
异常处理中的块,始终执行 |
finalize |
Object的方法,GC时调用(已废弃) |
考点2:try-catch-finally的执行顺序
public static int test() {
try {
return 1;
} finally {
return 2; // finally的return会覆盖try的return
}
}
// 返回2(但不建议在finally中return)
考点3:受检异常vs不受检异常
// 受检异常:IOException、SQLException等
// 不受检异常:RuntimeException及其子类
// 编译器只检查受检异常
考点4:throw和throws的区别
| 关键字 | 位置 | 作用 |
|---|---|---|
throw |
方法体内部 | 手动抛出异常对象 |
throws |
方法签名上 | 声明方法可能抛出的异常 |
参考资料
七、总结
今天我们学习了:
- ✅ Java异常体系的层次结构
- ✅ 五种异常处理方式
- ✅ 自定义异常的写法
- ✅ try-with-resources自动关闭资源
- ✅ 异常处理的最佳实践
重点记忆:
- Error不需要处理,Exception需要处理
- 受检异常必须try-catch或throws
- finally始终执行(System.exit除外)
- 生产环境不要用e.printStackTrace()
- 推荐用try-with-resources管理资源
下一步预告:
Day12我们将学习集合框架(上)——ArrayList、LinkedList等List家族。
互动话题:你在实际开发中,遇到过哪些让你印象深刻的异常?欢迎在评论区分享!
如果这篇文章对你有帮助,欢迎点赞、收藏!这是【JavaSE全面教学】系列的第11篇,关注我看完整套教程 👇
本文为【JavaSE全面教学】系列第11篇,持续更新中…
更多推荐



所有评论(0)