Java异常机制是项目容错、程序健壮性的核心保障,也是后端面试、工程开发的高频核心考点。程序运行中出现的非法参数、空指针、数组越界、IO失败等问题,全部依赖异常机制捕获与处理,避免程序直接崩溃退出。
本文全覆盖核心知识点:异常体系分类、try-catch-finally语法、throw/throws关键字、自定义业务异常、异常链传递,搭配完整可运行案例、执行顺序源码、高频坑点、工程规范、面试标准答案,适配零基础学习、项目落地、面试刷题。
一、Java异常体系概述
1.1 异常核心定义
异常(Exception):程序在编译或运行过程中出现的非致命错误,通过异常机制可以捕获、处理、兜底,保证程序继续运行。
错误(Error):系统级致命问题,无法手动处理,程序直接崩溃(如OOM内存溢出、栈溢出)。
1.2 异常顶层继承体系(面试必考)
Java异常所有类顶层父类:java.lang.Throwable
-
Error(错误):系统级异常,虚拟机抛出,代码无需处理、无法处理
-
Exception(异常):代码级异常,可捕获、可处理,分为两大类
1.2.1 编译时异常(受检异常 CheckedException)
除RuntimeException外所有Exception子类,编译阶段必须处理,否则代码报错无法运行。
常见场景:IO读写、文件操作、数据库连接、反射、线程异常
1.2.2 运行时异常(非受检异常 RuntimeException)
程序运行阶段才会抛出,编译不报错,可选择性捕获处理,是开发中最常见异常。
常见场景:空指针、数组越界、类型转换异常、算术异常、参数不合法
二、核心捕获语法:try-catch-finally
try-catch-finally是Java唯一的异常捕获处理语法,用于主动拦截异常、兜底容错,保证程序不崩溃。
2.1 语法结构与执行流程
-
try:包裹可能出现异常的核心业务代码
-
catch:捕获对应类型异常,做异常处理、日志打印、参数兜底
-
finally:无论是否出现异常,一定会执行,用于资源关闭(IO、流、数据库连接)
2.2 基础实战代码
public class TryCatchDemo {
public static void main(String[] args) {
try {
// 可能出现异常的代码
int a = 1 / 0;
System.out.println("正常执行代码");
} catch (ArithmeticException e) {
// 捕获算术异常,自定义处理逻辑
System.out.println("算术异常:除数不能为0");
// 打印异常堆栈信息(开发必备)
e.printStackTrace();
} finally {
// 无论是否异常,必然执行
System.out.println("finally 资源释放");
}
// 异常被捕获,程序继续执行,不会崩溃
System.out.println("程序正常结束");
}
}
2.3 多catch捕获规则
一段代码可能抛出多种异常,可配置多个catch块,遵循:子类异常在前,父类异常在后。
public class MultiCatchDemo {
public static void main(String[] args) {
try {
String str = null;
str.length();
int[] arr = new int[3];
arr[5] = 10;
} catch (NullPointerException e) {
System.out.println("捕获空指针异常");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("捕获数组越界异常");
} catch (Exception e) {
// 父类异常兜底,捕获所有未知异常
System.out.println("捕获全局异常");
}
}
}
2.4 finally 高频致命坑点(面试必考)
-
唯一不执行finally的场景:try/catch中执行
System\.exit\(0\)终止JVM -
finally中禁止写return!会覆盖try/catch的return返回值,掩盖异常
-
finally核心用途:资源关闭、连接释放、事务收尾,不写业务逻辑
2.5 try-catch-finally 返回值坑点实战
public class FinallyReturnDemo {
public static int getNum() {
try {
return 10;
} finally {
// finally return 会覆盖上层返回值
return 20;
}
}
public static void main(String[] args) {
// 最终输出20,严重BUG!禁止在finally写return
System.out.println(getNum());
}
}
三、抛出异常:throw & throws(核心区别)
很多场景无需当场捕获异常,需要向上抛给调用者处理,此时使用 throw、throws 关键字,二者是面试高频对比考点。
3.1 throws(声明抛出异常)
作用:标记在方法签名上,声明当前方法可能抛出异常,抛给方法调用者处理。
适用场景:工具方法、底层通用方法,自身不处理异常,交由业务层处理。
public class ThrowsDemo {
// 声明抛出编译时异常,交由调用者处理
public static void readFile() throws Exception {
// 文件读取操作,存在编译时异常
System.out.println("读取文件");
}
public static void main(String[] args) {
// 方式1:调用者手动try-catch捕获
try {
readFile();
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.2 throw(手动抛出异常对象)
作用:写在方法内部,主动手动创建异常对象并抛出,中断当前逻辑,用于主动校验参数、拦截非法业务。
public class ThrowDemo {
public static void checkAge(int age) {
if (age < 0 || age > 150) {
// 手动主动抛出运行时异常
throw new RuntimeException("年龄参数不合法!");
}
System.out.println("年龄校验通过");
}
public static void main(String[] args) {
checkAge(-10);
}
}
3.3 throw vs throws 终极面试对比
| 对比维度 | throws | throw |
|---|---|---|
| 位置 | 方法签名后 | 方法内部 |
| 作用 | 声明异常、告知调用者 | 主动抛出异常对象、中断逻辑 |
| 数量 | 可声明多个异常 | 一次只能抛出一个异常 |
| 本质 | 被动声明 | 主动触发 |
四、自定义异常(工程必备)
JDK自带异常仅能满足通用场景,业务系统需要自定义业务异常,用于区分不同业务报错、精准返回前端提示、统一异常响应格式,是企业项目标准化规范。
4.1 自定义异常分类
-
自定义运行时异常(推荐):继承 RuntimeException,无需强制捕获,业务开发首选
-
自定义编译时异常:继承 Exception,必须强制捕获或抛出,极少使用
4.2 工程级自定义业务异常(可直接商用)
// 自定义全局业务异常
public class BusinessException extends RuntimeException {
// 异常码 + 异常信息(前后端对接核心)
private Integer code;
private String msg;
// 构造方法
public BusinessException(Integer code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
// 预设常用业务异常(统一规范)
public static BusinessException PARAM_ERROR() {
return new BusinessException(400, "参数非法");
}
public static BusinessException USER_NOT_EXIST() {
return new BusinessException(404, "用户不存在");
}
// getter
public Integer getCode() { return code; }
public String getMsg() { return msg; }
}
4.3 自定义异常实战使用
public class ExceptionBizDemo {
// 业务校验方法
public static void login(String username) {
if (username == null || "".equals(username)) {
// 主动抛出自定义业务异常
throw BusinessException.PARAM_ERROR();
}
if (!"admin".equals(username)) {
throw BusinessException.USER_NOT_EXIST();
}
}
public static void main(String[] args) {
try {
login("");
} catch (BusinessException e) {
// 精准捕获业务异常,返回对应错误码和信息
System.out.println("错误码:" + e.getCode());
System.out.println("错误信息:" + e.getMsg());
}
}
}
工程价值:统一项目报错格式、方便前端解析、精准定位业务问题、避免杂乱原生异常信息。
五、异常链(异常溯源与传递)
异常链:将底层抛出的原始异常,封装为上层业务异常向上传递,保留完整异常堆栈溯源信息,解决多层调用异常丢失、无法定位根因的问题。
核心场景:多层接口调用、分层架构(DAO-Service-Controller)、异常二次封装。
5.1 异常链核心原理
通过异常构造方法 super\(message, cause\),传入原始异常对象,绑定异常根因,形成异常链路,完整保留报错堆栈。
5.2 异常链实战代码
// 带异常链的自定义业务异常
public class ChainBusinessException extends RuntimeException {
public ChainBusinessException(String msg, Throwable cause) {
// 绑定原始异常,形成异常链
super(msg, cause);
}
}
public class ExceptionChainDemo {
// 底层DAO层方法抛出原始异常
public static void daoMethod() {
int a = 1 / 0;
}
// 上层Service层封装异常
public static void serviceMethod() {
try {
daoMethod();
} catch (Exception e) {
// 封装原始异常,向上传递,保留根因
throw new ChainBusinessException("业务查询失败", e);
}
}
public static void main(String[] args) {
try {
serviceMethod();
} catch (Exception e) {
// 打印完整异常链,可定位到底层原始报错
e.printStackTrace();
}
}
}
5.3 异常链核心作用
-
保留原始异常根因,避免上层封装后丢失底层报错信息
-
分层解耦,底层技术异常封装为上层业务异常
-
线上问题快速溯源,精准定位报错代码行数
六、Java异常工程最佳实践 & 避坑指南
-
禁止空catch块:捕获异常不处理、不打印日志,线上问题无法排查
-
精准捕获异常:不直接捕获Exception,优先捕获具体子类异常,避免掩盖未知BUG
-
finally只做资源释放:禁止写业务逻辑、禁止return、禁止抛异常
-
业务优先自定义异常:统一错误码、统一响应格式,适配前后端分离项目
-
多层调用必须维护异常链:封装异常时绑定原始cause,保留堆栈信息
-
运行时异常不滥用throws:避免接口签名臃肿,统一全局异常捕获处理
七、全文核心总结(面试必背)
-
异常体系:Throwable分为Error系统错误、Exception代码异常,Exception包含编译时异常、运行时异常
-
try-catch-finally:try捕获异常、catch处理异常、finally必执行(资源释放专用)
-
throw:方法内主动抛出异常对象,手动中断业务逻辑
-
throws:方法签名声明异常,将异常抛给调用者处理
-
自定义异常:继承RuntimeException,自定义错误码和信息,统一业务报错规范
-
异常链:封装上层异常时绑定底层原始异常,保留完整堆栈,实现异常溯源
八、高频面试简答题
-
finally一定会执行吗? 不一定,执行System.exit(0)退出JVM时,finally不会执行。
-
throw和throws的区别? throw是方法内主动抛异常对象;throws是方法签名声明异常,被动交由调用者处理。
-
运行时异常和编译时异常区别? 编译时异常编译强制处理,多用于IO、反射;运行时异常运行抛出,编译不报错,多用于业务参数校验。
-
为什么需要自定义异常? 统一项目错误格式、区分业务报错类型、方便前后端对接、精准定位线上问题。
-
什么是异常链?作用是什么? 上层异常封装底层原始异常,保留完整堆栈信息,解决多层调用异常丢失、无法溯源的问题。
-
catch Exception 有什么弊端? 会捕获所有异常,掩盖未知BUG,不利于问题排查,开发中优先精准捕获子类异常。
-
finally中为什么不能写return? 会覆盖try/catch的返回值,篡改方法返回结果,掩盖异常信息,造成隐蔽BUG。