7.2 接管差错:try-catch-finally与自动关闭


7.2 接管差错:try-catch-finally 与自动关闭

本节摘要:try 块装可能出错的代码,catch 按类型匹配接管(从小到大排、父类兜底放最后),finally 无论成败必执行且能压过 try 里的 return。带资源的 try 要求资源实现自动关闭接口,按声明逆序关闭,替代手写 finally 关流的样板。本节用两个实验验证执行顺序的细节,并总结四类常见反模式。

catch 是怎么匹配的

先看基本盘。try 里出异常时,运行时从上到下逐个检查 catch,第一个类型匹配的接管,匹配规则就是继承关系——子类异常能被父类的 catch 接住。多个 catch 的排序因此有铁律:子类在前、父类在后,父类写前面会把子类的活全抢了,后面写子类编译直接报错"已捕获":

public class MatchDemo { public static void main(String[] args) { String[] names = {"登记", null}; try { System.out.println(names[1].length()); // null 调方法 抛空指针异常 System.out.println(names[5]); // 若上一行没炸 这行抛下标越界 } catch (java.util.NoSuchElementException e) { // 更具体的类型放前面 System.out.println("不会命中这种"); } catch (NullPointerException e) { // 命中:空指针是运行时异常的子类 System.out.println("接管空指针:检查数据来源"); } catch (RuntimeException e) { // 父类兜底放最后 System.out.println("兜底运行时异常"); } System.out.println("程序继续运行"); // 输出:程序继续运行 } }

实际输出:接管空指针那一行加"程序继续运行"。匹配上之后,其余 catch 全部跳过,流程从 catch 块结束后继续——异常被接住,中断就被解除,这正是"接管"的含义。

return 撞上 finally:三个实验

finally 的语义是"无论 try 正常结束、出异常、还是 return,都要执行"。它常被用来做收尾(关流、释放锁、记日志),但有两个细节必须动手验证过才敢说懂:

public class FinallyDemo { // 实验一:finally 在 return 之后、方法真正返回之前执行 static int labOne() { try { return 1; } finally { System.out.println("实验一的 finally 执行了"); } } // 实验二:基本类型 finally 里改返回值变量 改不动 static int labTwo() { int x = 1; try { return x; // 返回值在这里已经定下 } finally { x = 99; // 改的是局部变量 改不了已定下的返回值 System.out.println("实验二的 finally 执行了"); } } // 实验三:finally 里自己 return 会吞掉 try 的返回值与异常 —— 绝对禁令 static int labThree() { try { int a = 1 / 0; // 这里会抛算术异常 return 1; } finally { return 99; // 禁令示范:finally 的 return 强行接管 } } public static void main(String[] args) { System.out.println(labOne()); // 先输出 finally 语句 再输出:1 System.out.println(labTwo()); // 先输出 finally 语句 再输出:1(不是 99) System.out.println(labThree()); // 输出:99 —— 异常被吞了 调用方毫无感知 } }

三个实验合起来读:finally 一定执行(实验一);try 里 return 的返回值在离开 try 时已定格,finally 改基本类型变量改不动它(实验二);finally 里写 return 会把 try 的返回值乃至异常整个吞掉(实验三)——所以工程铁律是 finally 里永远不写 return, lint 工具见了都会报警。另外两个"finally 不执行"的极端情形顺带记下:虚拟机退出(第 4 章 System.exit)与所在线程被杀——日常代码几乎遇不到,知道有这回事即可。

带资源的 try:把样板代码交给语法

第 6 章反复用的 try 后跟圆括号,现在拆开讲。任何实现了自动关闭接口的资源(各种流、连接都实现了)都能放进 try 的括号里:无论正常结束还是出异常,编译器自动生成关闭代码,且多个资源按声明的逆序关闭(后开的先关,像套娃拆封):

import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class ResourceDemo { public static void main(String[] args) { // 手写版:finally 里判空关流 十行样板代码 BufferedReader manual = null; try { manual = new BufferedReader(new FileReader("note.txt")); System.out.println("手写版首行:" + manual.readLine()); } catch (IOException e) { System.out.println("接管:" + e.getMessage()); } finally { if (manual != null) { try { manual.close(); // close 自身也可能抛异常 又要一层 try } catch (IOException e) { System.out.println("关闭失败"); } } } // 自动关闭版:同样的语义 三行搞定 try (BufferedReader auto = new BufferedReader(new FileReader("note.txt"))) { System.out.println("自动版首行:" + auto.readLine()); } catch (IOException e) { System.out.println("接管:" + e.getMessage()); } } }

两个版本语义等价,自动版少了大半样板。细节两处:逆序关闭是资源依赖的必然——先开的流可能被后开的流包裹(缓冲流套文件流),必须先拆外层再拆内层;关闭时若也出异常(主异常之后又抛关闭异常),后者会被挂为"被抑制的异常",主异常不丢——手写版反而容易把主异常覆盖掉,这是自动版不太显眼但实在的优势。

四类常见反模式

处置流程的坑不止语法,更多是姿势。四类高频反模式,见到就要改:

反模式 长相 危害 正解
吞异常 catch 里什么都不写 程序带病运行 错误无声累积 至少记日志与上下文 必要时上报
一把抓 直接 catch Exception 或 Throwable 把本该暴露的缺陷全遮住 排错失明 catch 具体类型 分支处理
finally 里 return 收尾代码里强行返回 吞掉返回值与异常(实验三) finally 只做清理 不碰返回
异常当流程 用抛异常代替条件判断跳转 异常构造带堆栈 成本高 语义乱 正常分支用返回值 异常只用于异常

完整案例背景:配置读取模块吞异常——catch 块空着,配置缺失时系统用默认值继续跑,三天后报表全错却无人知晓。操作:改成"记日志加默认值加计数"的三件套,并在监控里对"配置降级次数"报警:

import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class ConfigCase { static int fallbackCount = 0; static String readFirstLine(String path, String fallback) { try (BufferedReader r = new BufferedReader(new FileReader(path))) { return r.readLine(); // 正常路径:读到配置 } catch (IOException e) { fallbackCount++; // 降级计数 供监控报警 System.out.println("配置读取失败 使用默认值 原因:" + e.getMessage()); return fallback; // 有预案的降级 不是吞异常 } } public static void main(String[] args) { System.out.println(readFirstLine("缺失的配置.txt", "默认端口8080")); // 输出:配置读取失败 使用默认值 原因:缺失的配置.txt // 默认端口8080 System.out.println("累计降级次数:" + fallbackCount); // 输出:累计降级次数:1 } }

结果:降级路径与正常路径都留下痕迹,问题可被监控发现。解读:吞异常与"降级"的区别就在痕迹——降级是有预案、可观测的接管;吞异常是把问题推进地毯下。变式:若配置缺失属于"系统无法工作"级别,catch 里不该降级而应抛出自定义异常终止启动(下一节正是自定义异常的主场);若调用方有能力修复(比如提示重选路径),把异常往上 throws 比就地消化更合适——接不接、报不报,取决于谁最有能力处理,这是异常设计的核心判断。

本节要点回顾

  • catch 从上到下匹配第一个命中的:子类在前父类兜底在后,顺序反了编译报错;接住即解除中断
  • finally 三个实验的结论:一定执行(除非退出或线程被杀);改不动已定格的基本类型返回值;finally 里 return 会吞返回值与异常——绝对禁令
  • 带资源的 try:资源实现自动关闭接口、按声明逆序关闭、关闭异常挂为被抑制而不覆盖主异常——替代全部手写样板
  • 四反模式:吞异常、一把抓、finally 里 return、异常当流程——见一个改一个
  • 接管还是上报看谁有能力处理:可降级就地接、致命问题抛出去、调用方能修就上交

标准处置流程齐了,还剩最后一件事:差错条目不够用时怎么办。下一节立自己的条目——throw 与 throws 的分工、自定义异常的两层结构与错误码设计。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U