Java 中异常分哪两类,有什么区别?
2026年03月18日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
体系理解:面试官不仅仅是想知道 "受检异常" 和 "非受检异常" 这两个名词,更是想考察你是否理解 Java 异常体系的继承结构(
Throwable→Error/Exception)。 -
编译机制:受检异常的 "受检" 是什么意思?这涉及 Java 编译器如何强制处理异常。
-
实践能力:实际开发中如何选择异常类型?何时用受检异常、何时用非受检异常?
核心答案
Java 异常分为受检异常和非受检异常两大类。受检异常必须显式处理(try-catch 或 throws),否则编译不通过;非受检异常可以不处理,由 JVM 在运行时抛出。
| 对比维度 | 受检异常 | 非受检异常 |
|---|---|---|
| 父类 | Exception(除 RuntimeException) | Error、RuntimeException |
| 编译检查 | ✅ 必须处理,否则编译失败 | ❌ 不强制处理 |
| 处理方式 | try-catch 或 throws | 可选处理 |
| 代表场景 | IO 错误、数据库异常 | 空指针、数组越界、内存溢出 |
| 设计理念 | 可恢复的异常 | 不可恢复或程序 bug |
一句话总结:受检异常是 "编译器逼你处理",非受检异常是 "运行时随缘处理"。
深度解析
一、Java 异常体系结构
上图展示了 Java 异常体系的完整继承结构。关键要点:
Throwable:所有异常的父类,只有它的子类才能被throw或catchError:JVM 级别的严重错误,程序无法恢复(如OutOfMemoryError、StackOverflowError)Exception:程序可以处理的异常- 受检异常:
IOException、SQLException等,编译器强制处理 - 非受检异常:
RuntimeException及其子类,编译器不强制处理
- 受检异常:
二、受检异常 vs 非受检异常
import java.io.*;
public class ExceptionDemo {
// ========== 受检异常:必须处理 ==========
public void readCheckedFile(String path) {
// ❌ 编译错误:Unhandled exception type IOException
// FileReader reader = new FileReader(path);
// ✅ 方式一:try-catch 捕获
try {
FileReader reader = new FileReader(path);
} catch (IOException e) {
e.printStackTrace();
}
}
// ✅ 方式二:throws 声明抛出
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
}
// ========== 非受检异常:不强制处理 ==========
public void uncheckedDemo() {
String str = null;
// ⚠️ 编译通过,但运行时抛出 NullPointerException
System.out.println(str.length());
// 可以选择捕获,也可以不捕获
try {
System.out.println(str.length());
} catch (NullPointerException e) {
System.out.println("空指针异常");
}
}
}
三、常见异常类型
| 类型 | 异常类 | 触发场景 |
|---|---|---|
| Error(非受检) | OutOfMemoryError | 内存不足 |
StackOverflowError | 栈溢出(无限递归) | |
NoClassDefFoundError | 类找不到 | |
| RuntimeException(非受检) | NullPointerException | 访问 null 对象 |
ArrayIndexOutOfBoundsException | 数组越界 | |
ClassCastException | 类型转换失败 | |
ArithmeticException | 算术异常(如除零) | |
IllegalArgumentException | 非法参数 | |
| 受检异常 | IOException | IO 操作失败 |
SQLException | 数据库操作失败 | |
ClassNotFoundException | 类加载失败 | |
InterruptedException | 线程中断 |
四、如何选择异常类型?
上图展示了选择异常类型的决策流程。关键原则:
- 受检异常:调用者可以合理恢复的情况(如文件不存在、网络超时)
- 非受检异常:程序 bug 或调用者无法处理的情况(如空指针、数组越界)
- Error:JVM 级别的严重问题,程序不应尝试捕获
五、最佳实践
// ✅ 好的做法:自定义业务异常(继承 RuntimeException)
public class BusinessException extends RuntimeException {
private String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}
// ✅ 好的做法:受检异常转非受检异常(避免污染方法签名)
public void processFile(String path) {
try {
readFile(path);
} catch (IOException e) {
// 将受检异常包装为非受检异常
throw new RuntimeException("文件处理失败", e);
}
}
// ❌ 不好的做法:捕获异常后不处理(吞掉异常)
try {
doSomething();
} catch (Exception e) {
// 什么都不做,异常被吞掉了!
}
// ✅ 好的做法:至少记录日志
try {
doSomething();
} catch (Exception e) {
log.error("操作失败", e);
throw new BusinessException("OPERATION_FAILED", "操作失败");
}
面试高频追问
-
throw和throws有什么区别?关键字 位置 作用 throw方法体内 抛出一个异常对象 throws方法签名上 声明方法可能抛出的异常类型 -
final、finally、finalize有什么区别?final:修饰符,类不能继承、方法不能重写、变量不能修改finally:try-catch的最后一块,无论是否异常都会执行finalize:Object的方法,GC 回收对象前调用(已废弃)
-
为什么要有受检异常?有人说它是 Java 的设计失误,你怎么看?
争议点:
- 支持者:强制处理异常,提高代码健壮性
- 反对者:导致代码冗余,异常传播污染方法签名,实际中常被滥用
- 趋势:现代框架(Spring)倾向于使用非受检异常
常见面试变体
- 变体一:"
Error和Exception有什么区别?" - 变体二:"
RuntimeException和普通Exception有什么区别?" - 变体三:"如何设计一个自定义异常类?"
记忆口诀
受检异常编译查,必须 try-catch 或 throws; 非受检异常运行时,不强制处理随缘抓; Error 严重难恢复,Exception 可处理。
总结
Java 异常分为受检异常(必须显式处理)和非受检异常(不强制处理)。受检异常适合可恢复的场景,非受检异常适合程序 bug 或无法恢复的场景。实际开发中,推荐使用非受检异常(继承 RuntimeException)设计业务异常,避免受检异常污染方法签名。