Java异常处理总结
作者:没有四次元口袋的蓝胖
日期:2026-06-09
标签:Java, 异常处理
一、异常体系架构
1.1 整体继承树
Throwable ├── Error(严重错误,程序不该捕获) │ ├── StackOverflowError │ ├── OutOfMemoryError │ └── ... └── Exception(可处理的异常) ├── RuntimeException(非检查型/未检查异常) │ ├── NullPointerException │ ├── ArrayIndexOutOfBoundsException │ ├── ClassCastException │ ├── ArithmeticException │ ├── NumberFormatException │ └── ... └── 非RuntimeException(检查型异常/受检异常) ├── IOException ├── SQLException ├── FileNotFoundException ├── ClassNotFoundException └── ...1.2 三大分支一句话区分
| 类型 | 特点 | 举例 | 要不要处理 |
|---|---|---|---|
| Error | JVM层面的严重问题,程序无法恢复 | OOM、栈溢出 | 不该捕获,只能预防 |
| 受检异常(Checked) | 编译器强制要求处理,不处理编译不通过 | IOException、SQLException | 必须try-catch或throws |
| 非受检异常(Unchecked) | 编译器不强制,运行时才暴露 | NPE、数组越界、类型转换 | 建议处理,但不强制 |
面试必问:Error和Exception的区别?
→ Error是程序无法处理的严重问题(JVM级),Exception是程序可以处理的异常。实际开发中我们只处理Exception,Error出现了基本就是代码有bug或资源不够。
面试追问:受检异常和非受检异常的区别?
→ 核心区别是编译器是否强制处理。受检异常必须在代码中显式try-catch或throws声明,否则编译失败;非受检异常(RuntimeException及其子类)编译器不强制,但运行时一样会抛出。设计上,受检异常是"可预期的合理异常"(如文件找不到),非受检异常是"编程错误"(如空指针)。
二、异常处理的五种方式
2.1 try-catch-finally
try{// 可能出问题的代码intresult=10/0;}catch(ArithmeticExceptione){// 处理特定异常System.out.println("算术异常:"+e.getMessage());}catch(Exceptione){// 处理其他异常(子类在前,父类在后)System.out.println("其他异常:"+e.getMessage());}finally{// 无论如何都执行(除非JVM退出)System.out.println("释放资源");}catch顺序规则:子类在前,父类在后。如果把Exception写在ArithmeticException前面,编译报错——因为Exception已经涵盖了子类,后面的catch永远走不到,这是"不可达代码"。
2.2 try-catch-finally的执行顺序——面试高频陷阱
情况1:try正常执行,无异常
try → finally情况2:try有异常,catch匹配到
try(异常处中断)→ catch → finally情况3:try有异常,catch没匹配到
try(异常处中断)→ finally → 异常向上抛情况4:catch中也有异常
try → catch(新异常)→ finally → catch中的新异常向上抛情况5:finally中有return——绝对不要这么写
publicstaticinttest(){try{return1;}finally{return2;// 会覆盖try中的return 1}}// 结果:返回2,且try中的返回值被吞掉面试经典题:
publicstaticinttest(){inta=10;try{a=20;returna;// 此时已经确定了返回值20的"快照"}finally{a=30;// 修改了a,但不影响返回值}}// 结果:返回20,不是30结论:finally中的修改不会影响try/catch中已经确定的返回值(基本类型)。但如果是引用类型,修改对象的属性是会影响返回结果的。
2.3 throws——声明异常
// 方法签名上声明可能抛出的异常,交给调用者处理publicvoidreadFile(Stringpath)throwsIOException,FileNotFoundException{// 读取文件}throws不是"处理"异常,是"甩锅"——告诉调用者"我这个方法可能出这些异常,你来处理"。调用者要么继续throws,要么try-catch。
throws的规则:
- 受检异常必须声明,非受检异常可选(但建议也写上,文档意义)
- 重写方法时,子类throws的异常范围不能超过父类(可以更小或不抛)
2.4 throw——主动抛出异常
publicvoidsetAge(intage){if(age<0||age>150){thrownewIllegalArgumentException("年龄不合法:"+age);}this.age=age;}throw vs throws 区别:
| 对比 | throw | throws |
|---|---|---|
| 位置 | 方法体内部 | 方法签名上 |
| 作用 | 主动抛出一个异常对象 | 声明方法可能抛出的异常类型 |
| 数量 | 一次只能抛一个 | 可以声明多个 |
| 跟谁配合 | 后面跟异常对象new XxxException() | 后面跟异常类名 |
2.5 try-with-resources(JDK 7+)——自动关闭资源
// 传统写法:啰嗦,容易漏关Connectionconn=null;try{conn=DriverManager.getConnection(url);// 使用连接}catch(SQLExceptione){e.printStackTrace();}finally{if(conn!=null){try{conn.close();// close也可能抛异常!}catch(SQLExceptione){e.printStackTrace();}}}// try-with-resources:简洁、安全try(Connectionconn=DriverManager.getConnection(url);Statementstmt=conn.createStatement();ResultSetrs=stmt.executeQuery(sql)){// 使用资源,结束后自动关闭}catch(SQLExceptione){e.printStackTrace();}// 无需finally,自动按声明的逆序关闭原理:资源必须实现AutoCloseable接口(或其父接口Closeable),try结束时自动调用close()方法。
JDK 9增强:允许在try外声明变量,直接引用
Connectionconn=DriverManager.getConnection(url);try(conn){// JDK 9+,直接引用已声明的变量// 使用连接}面试追问:“try-with-resources中如果try块和close都抛异常,哪个生效?”
→ try块的异常是主异常,close的异常会被抑制(suppressed),通过Throwable.getSuppressed()可以获取。这是try-with-resources比手动finally关闭的一个重要优势——finally中close的异常会覆盖try中的原始异常,导致问题难以定位。
三、常见异常速查
3.1 非受检异常(RuntimeException家族)
| 异常 | 触发场景 | 预防方式 |
|---|---|---|
| NullPointerException | 调用null对象的成员 | 前置判空 / Optional |
| ArrayIndexOutOfBoundsException | 数组下标越界 | 检查length |
| ClassCastException | 强制类型转换失败 | 先用instanceof判断 |
| ArithmeticException | 整数除以0 | 除前检查 |
| NumberFormatException | 字符串转数字失败 | 正则预校验或try-catch |
| StringIndexOutOfBoundsException | 字符串下标越界 | 检查length |
| ConcurrentModificationException | 遍历集合时修改集合 | 用迭代器删除 / CopyOnWrite |
| IllegalArgumentException | 参数不合法 | 参数校验 |
3.2 受检异常(编译器强制处理)
| 异常 | 触发场景 |
|---|---|
| IOException | IO操作失败(文件、网络) |
| FileNotFoundException | 文件不存在 |
| SQLException | 数据库操作失败 |
| ClassNotFoundException | 类加载失败 |
| InterruptedException | 线程被中断 |
| CloneNotSupportedException | 不支持克隆 |
四、自定义异常
4.1 为什么要自定义?
JDK的异常类是通用的,无法表达业务语义。比如"余额不足"用ArithmeticException显然不合适,自定义InsufficientBalanceException一眼就懂。
4.2 怎么自定义?
// 自定义受检异常publicclassBusinessExceptionextendsException{privateintcode;publicBusinessException(Stringmessage){super(message);}publicBusinessException(intcode,Stringmessage){super(message);this.code=code;}publicintgetCode(){returncode;}}// 自定义非受检异常publicclassParamInvalidExceptionextendsRuntimeException{publicParamInvalidException(Stringmessage){super(message);}}选型:继承Exception还是RuntimeException?
- 业务校验失败(参数错误、余额不足)→ 继承RuntimeException,不强制上层处理,由全局异常处理器统一捕获
- 必须让调用者处理的异常 → 继承Exception
- 实际开发中,自定义异常大部分继承RuntimeException,配合Spring的
@ControllerAdvice全局处理
五、异常处理最佳实践
5.1 七个不要
① 不要吞掉异常
// 反例:异常被吞,出问题完全没线索try{doSomething();}catch(Exceptione){// 空catch块,最差写法}// 正例:至少记录日志try{doSomething();}catch(Exceptione){log.error("操作失败",e);}② 不要用异常控制流程
// 反例:用异常代替条件判断,性能差、可读性差try{list.get(index);}catch(IndexOutOfBoundsExceptione){// 处理越界}// 正例:先检查条件if(index>=0&&index<list.size()){list.get(index);}③ 不要在finally中return——前面已经讲过,会覆盖原始返回值和异常。
④ 不要catch范围过大
// 反例:一把梭Exceptioncatch(Exceptione){...}// 正例:捕获具体类型catch(IOExceptione){...}catch(SQLExceptione){...}⑤ 不要重复记录同一异常——catch中log一次,throws出去调用者又log一次,日志里全是重复堆栈。
⑥ 不要用e.printStackTrace()——生产环境应该用日志框架(SLF4J/Logback),printStackTrace输出到System.err,不可控。
⑦ 不要在循环里频繁try-catch——把try-catch移到循环外面,除非每次迭代的异常需要单独处理。
5.2 五个要
① 要用try-with-resources管理资源——替代手动finally关闭,简洁且不会丢失原始异常。
② 要给异常带上下文信息
// 反例:只抛异常,不知道哪个文件出问题thrownewFileNotFoundException();// 正例:带上具体信息thrownewFileNotFoundException("配置文件不存在: "+configPath);③ 要在合适的层次处理异常——底层抛出,业务层捕获并转换,最上层统一响应。不要在底层吞掉,也不要在最底层就返回用户错误码。
④ 要优先校验而非依赖异常——前置条件检查比事后捕获更高效、更清晰。
⑤ 要对受检异常做合理包装
// 反例:层层throws IOException,上层被迫处理publicvoidbusinessMethod()throwsIOException{...}// 正例:包装成业务异常publicvoidbusinessMethod(){try{readFile();}catch(IOExceptione){thrownewBusinessException("读取配置失败",e);// 保留原始异常链}}注意保留异常链!new BusinessException("msg", e)把原始异常e传进去,这样日志里能看到完整调用链。如果只写new BusinessException("msg")就丢了根因。
六、面试必背题
Q1:Java异常体系的顶层类是什么?分几大类?
Throwable,分Error和Exception。Exception再分受检(Checked)和非受检(Unchecked/RuntimeException)。
Q2:finally块一定执行吗?
几乎一定,但有例外:
System.exit(0)杀掉JVM,finally不执行- 执行try/catch的线程被杀死或中断
- 守护线程中的finally可能不会执行(JVM退出时)
Q3:try里有return,finally还执行吗?返回值是什么?
执行。finally在return之后、方法真正返回之前执行。如果finally中没有return,不影响try/catch中的返回值(基本类型);如果finally中有return,会覆盖原返回值。
Q4:受检异常和非受检异常的区别?举例说明。
受检异常编译器强制处理(try-catch或throws),如IOException;非受检异常编译器不强制,如NullPointerException。设计意图:受检异常是"可预期的外部问题",非受检异常是"编程错误"。
Q5:throw和throws的区别?
throw在方法体内,主动抛出一个异常对象;throws在方法签名上,声明该方法可能抛出的异常类型。throw一次一个,throws可以声明多个。
Q6:try-with-resources比try-finally好在哪里?
- 代码简洁,不需要手动关闭
- 自动按声明逆序关闭多个资源
- close的异常不会覆盖try中的原始异常(被suppressed保留)
- 不会因为忘写finally导致资源泄漏
思维导图速览
Java异常处理 ├── 一、体系架构 │ ├── Throwable │ │ ├── Error(不可处理:OOM、StackOverflow) │ │ └── Exception │ │ ├── 受检异常(必须处理:IOException) │ │ └── 非受检异常(不强制:NPE) │ └── 区别核心:编译器是否强制处理 ├── 二、五种处理方式 │ ├── try-catch-finally(执行顺序是面试重点) │ ├── throws(声明/甩锅) │ ├── throw(主动抛出) │ └── try-with-resources(自动关资源,优先用) ├── 三、常见异常 │ ├── RuntimeException:NPE/越界/类型转换/参数非法 │ └── Checked:IO/SQL/类加载/中断 ├── 四、自定义异常 │ ├── 继承Exception → 受检 │ └── 继承RuntimeException → 非受检(推荐) ├── 五、最佳实践 │ ├── 七不要:吞异常/异常控流程/finally return/... │ └── 五要:with-resources/带上下文/合适层次/... └── 六、面试必背六题 ├── 体系分类 ├── finally执行时机 ├── try里return与finally ├── 受检vs非受检 ├── throw vs throws └── try-with-resources优势写在最后
异常处理是Java基本功,面试考法很固定:体系结构→执行顺序→最佳实践。核心要记住:
- 体系图要能画出来——Throwable分Error和Exception,Exception再分受检和非受检,这是所有异常题的基础
- finally的执行顺序是高频考点——特别是"try中有return,finally还执行吗"和"finally中return会覆盖"这两道变体题
- try-with-resources要会写——JDK 7就有了,但很多人面试时还是只写try-finally,显得不够现代
- 最佳实践不是空话——“不要吞异常”"不要用异常控制流程"这些在代码审查中天天见,面试说了说明你写过生产代码