ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Java异常面试20题:从Throwable到try-with-resources的底层原理

Java异常面试20题:从Throwable到try-with-resources的底层原理 带人面试这几年Java异常几乎是必问题而且问法一年比一年刁钻。从最基础的Throwable下面分哪两类到finally里 return 会不会覆盖 try 里的返回值再到为什么不要在 finally 里抛异常每一层都能筛掉一批人。坦白说异常处理在大多数人日常代码里就是无脑 try-catch 的肌肉记忆但面试官想看的恰恰是你在异常机制上有没有真正的底层理解。这篇就按我实际带新人、做面试官时的经验把 Java 异常面试里最常被问到的20道经典面试题拆开来讲每一道都会说清楚考什么、坑在哪、怎么答才够深。1. 先搞清楚Java异常体系这5道题别再送分了1.1 体系结构Throwable、Exception、Error三者到底什么关系Java 里所有的异常和错误归根到底都是 Throwable 的子类。Throwable 这一层下面分两个大分支Exception 和 Error。Exception 里面又细分成 RuntimeException非受检异常和除了 RuntimeException 之外的其他受检异常。很多刚工作没两年的人能说出这个结论但一问到设计意图就卡壳。Throwable 这个类名字本身就很直白可以被抛出它定义了所有异常对象的公共能力message描述信息、cause导致当前异常的根本原因、stackTrace异常堆栈快照。不管是空指针还是数据库连接失败最后都归一成同一个模型这保证了 JVM 在处理异常时能用统一逻辑。面试官问体系结构通常背后还有一层意思你平时看代码时有没有关注过异常的类型归属。比如很多面试者会把 RuntimeException 和 Error 混为一谈其实两者在异常处理哲学上完全不同。RuntimeException 代表程序逻辑缺陷比如空指针、下标越界理论上可以通过修复代码避免Error 代表 JVM 或系统级的严重问题比如 OutOfMemoryError程序自己通常无法修复。你可以在代码里 catch OOM但基本治标不治本因为内存已经处于危险状态。顺带提一个比较冷门但面试官很喜欢的点Throwable 不仅仅能用来封装 Exception 和 Error你甚至可以把自定义的 Throwable 子类当作业务异常使用只要调用方愿意。但实际工程里几乎没人这么干约定俗成是继承 Exception 或 RuntimeException。如果你在回答体系结构时能补充说异常不只是一种错误报告机制也是一种控制流裁定的手段整个回答的深度立刻不一样。1.2 Checked与Unchecked面试官最爱的二分法受检异常Checked Exception和非受检异常Unchecked Exception的区别是 Java 异常面试的必考题。受检异常必须显式处理要么用 try-catch 包裹要么在方法签名里用 throws 声明非受检异常则没有这个限制编译器不会强制你去处理。为什么 Java 要专门设计这种强制性的区分我的理解是受检异常表达的是调用方需要为这种可预期的失败负责的场景。比如你打开一个文件文件不存在是常见业务风险编译器有义务提醒你处理。而 RuntimeException 表达的更多是程序内部逻辑有问题或状态根本不可用出了错不是调用方靠 try-catch 就能兜底的得靠修代码。面试时如果只答到这里只能拿及格分更高一档的回答要说到工程实践的变化趋势。我带的项目里新的业务代码几乎不再主动声明受检异常了而是统一抛 RuntimeException 的子类靠全局异常处理器统一捕获。原因很简单受检异常把处理异常的压力传递给了每一个调用方在多层调用链上导致大量被迫的 try-catch 或者 throws 污染代码可读性直线下降。但要注意这不代表受检异常没有存在价值像读写文件、网络通讯这类底层 IO 场景强制处理依然有它的合理性。我用一个表格帮人记忆两类异常的核心差异面试前过一遍很有用。对比维度Checked ExceptionUnchecked Exception父类位置Exception 下除 RuntimeException 之外RuntimeException 子类编译期强制处理必须 try-catch 或 throws不强制典型场景IO、网络、配置加载空指针、非法参数、下标越界设计意图可预期的业务/环境风险程序逻辑缺陷或不可恢复状态工程建议底层 IO 或强约束时使用业务系统推荐使用1.3 Error能catch吗能但踩过的坑比想象多很多人一听到 Error 就先入为主认为不能 catch这其实是错的。从语法层面catch (Error e) 完全合法JVM 也不会阻止你。但从工程角度我强烈不建议你去接 Error原因不是接不住而是接了也处理不了。拿 OutOfMemoryError 来说当它出现时堆内存可能已经耗尽你即使 catch 住想通过清理缓存释放内存大概率也来不及甚至 catch 之后 JVM 再触发 GC 也没有足够空间。StackOverflowError 更典型无限递归导致栈溢出之后线程的调用栈已经极深你再做什么都很难让程序恢复到安全状态。有一个实际教训可以分享某个后台任务为了避免崩溃在顶层用了 catch (Throwable e) 做兜底结果某次 OOM 被吞掉之后系统对外看起来还活着但内部内存已经濒临崩溃后续每一个请求都在失败边缘挣扎排查了很久才定位到是 OOM 被吞导致的可悲状态。所以我的建议是除非是热备切换、资源清理这类极端兜底场景否则顶层也别轻易 catch Throwable宁可让进程快速失败也不要带病运行。1.4 常见运行时异常背全清单就是送分运行时异常RuntimeException是面试题里最常被拿来举例的一类。下面这些是高频出现的我按触发场景整理了一下NullPointerException空引用访问成员变量、方法或数组元素时触发。最经典也最常见。IndexOutOfBoundsException访问数组或集合时下标越界注意区分 ArrayIndexOutOfBoundsException 和 StringIndexOutOfBoundsException。ClassCastException强制类型转换失败比如把 Object 转成不兼容的类型。ArithmeticException算术异常最常见的就是整数除零。IllegalArgumentException方法接收到了非法的参数比如数值范围不合法。IllegalStateException对象状态不满足操作前提比如已经关闭的连接再调用方法。NumberFormatException字符串转数字失败。UnsupportedOperationException调用了未实现的操作一般出现在只读集合上。记这个清单不能只背名字面试官往往会给出一个小场景让你判断会抛什么异常。比如HashMap 的 key 是自定义对象对象没有重写 hashCode 会怎样答案是可能正常工作但 hashCode 相等的对象会被放到同一个槽位性能变差。想靠背清单应付不了这种场景题平时写代码遇到异常时多看一眼类型和堆栈比死记硬背更有用。1.5 throw与throws一抛一声明的区别常常被问倒throw 和 throws 看起来只差一个字母但语义完全不同。throw 用在方法体内部表示实际抛出一个异常实例后面必须跟一个 Throwable 对象throws 用在方法签名上表示这个方法可能抛出哪些异常类型是把异常责任向上传递的声明。举个简单的对比public class ThrowVsThrows { public static void main(String[] args) { try { doSomething(); } catch (BusinessException e) { System.out.println(捕获到了业务异常); } } // 用 throws 声明可能抛出的受检异常 public static void doSomething() throws BusinessException { // 满足条件时实际抛出异常实例 throw new BusinessException(参数不合法); } }这里可以看出一个设计逻辑throws 解决的是够不到异常的问题——当前方法把异常抛给上层由上层决定怎么处理。而 throw 解决的是这里确实发生了异常情况的问题。面试时如果被问到能不能只 throw 不 throws答案是可以只要抛的是 RuntimeException 或它的子类。反过来能不能只 throws 不 throw也行因为 throws 只是一种声明不代表方法内一定要抛。很多面试者死记硬背throw 是动作、throws 是声明这没错但我会希望听到你把异常责任传递链这个概念讲出来throws 本质是在协议层面告诉调用方调用这里需要做什么心理准备。2. try-catch-finally执行机制最容易踩坑的5道题2.1 执行顺序finally一定执行吗别急着回答是这个问题的经典程度不用多说。标准流程是try 块正常执行完毕接着执行 finallytry 块抛异常先匹配 catchcatch 执行完再 finally没有匹配的 catchfinally 执行完后异常继续向上抛。很多人只知道finally 最后执行但被问到finally 一定执行吗就开始犹豫了。实际情况是只要 JVM 没有被强制终止finally 都会执行。触发强制终止的典型情况包括 System.exit() 调用、JVM 崩溃、机器断电。此外如果 try 块所在线程被中断或 killfinally 的执行也不可靠。还有一个容易忽略的点如果 try 块里出现无限循环finally 自然永远执行不到——这不是它不执行而是没有执行的机会。这里的原理是 finally 的设计目标保证资源释放、状态清理等收尾工作不因 try 块正常或异常而改变。所以写 finally 时要假设它一定会执行不要在里面写可能失败的操作也不要写耗时操作保持 finally 足够轻量。面试时对方还可能追问finally 里能不能修改返回值这句要小心。修改的行为要区分基本类型和引用类型。基本类型在 finally 里改返回值变量不会影响已经计算好的返回值引用类型则要看修改的是变量指向还是对象内部状态。对象内部状态变了外部拿到的引用自然感知到变化。这个点放到下一题细化。2.2 return和finally到底谁先谁后return 和 finally 的执行顺序光靠return 先计算值finally 后执行最后返回这句话不够得结合字节码层面理解。我在实操中发现很多人听到finally 在 return 之前执行后反而误解成finally 先执行、return 语句后执行这是错的。准确说法是return 表达式求值发生在 finally 执行之前但方法真正把值返回给调用方发生在 finally 执行完成后。看个例子public class ReturnFinallyOrder { public static void main(String[] args) { System.out.println(compute()); } static int compute() { int result 1; try { result 2; return result; // 先把返回值 2 存到临时位置 } finally { result 3; // 修改 result不影响已暂存的返回值 System.out.println(finally 执行result result); } } }这里输出会是finally 先打印 finally 执行result3然后方法返回 2。很多人以为返回 3实际是 2。原因就是 return 的求值动作在 finally 之前完成返回值已经被复制到临时槽位后续对局部变量的修改不影响它。但如果 return 的是一个引用类型比如 HashMapfinally 里往这个 map 里 put 数据调用方拿到的是同一对象会看到 put 后的内容。这个细节在面试里很有区分度能体现你对值传递和引用传递的理解。2.3 finally里写return一个能帮你加分也能踩坑的操作finally 里写 return 是绝对要被 interviewer 重点盘问的因为它会覆盖 try 或 catch 里的返回值。举例public static int overrideDemo() { try { throw new RuntimeException(业务出错); } catch (RuntimeException e) { return 100; } finally { return 200; } }这段代码运行时不会把异常传出去最终返回值是 200。也就是 catch 里 return 100 已经执行了但 finally 里的 return 200 把返回路径抢过去覆盖掉了。更严重的是try 抛出的异常也被吞掉了调用方完全不知道业务出错。这种代码在很多老项目里见过通常是为了在异常情况下给一个默认返回值看似稳妥实则后患无穷。一旦异常被吞掉线上故障排查时日志里什么都看不到问题定位成本剧增。我的建议是 finally 里只做清理和状态复位永远不要写 return也不要写会抛异常的业务逻辑。这不是理论洁癖是真金白银踩坑换来的教训。2.4 try-with-resources编译器帮你把close的正确姿势写好try-with-resources 是 Java 7 引入的语法糖它的核心价值不只是简洁而是解决了传统 finally close 的异常掩盖问题。先看传统写法的问题// 不推荐的传统写法 InputStream in null; try { in new FileInputStream(test.txt); // 读文件 } finally { if (in ! null) { in.close(); // 如果close抛异常会覆盖try块里的原始异常 } }如果 try 块里读文件时抛了 IOExceptionfinally 里 close 又抛了 IOException传统写法下小概率会把 close 的异常覆盖掉原始异常导致你看到的异常根本不是根因。而 try-with-resources 会自动处理抑制异常底层 IOException 和 close 的 IOException 都会记录close 的异常会被标记为 suppressed可以通过 Throwable.getSuppressed() 查出来。语法很简单try (FileInputStream in new FileInputStream(test.txt); BufferedInputStream bin new BufferedInputStream(in)) { // 使用资源 } catch (IOException e) { // 处理 }两个实现细节值得记住第一资源声明要放在 try 后的括号里并且资源必须实现 AutoCloseable第二多个资源的关闭顺序与声明顺序相反类似栈的后进先出。比如先声明 in 再声明 bin关闭时先关 bin再关 in。这个顺序的目的是保证依赖方向正确避免外层资源关闭后内层资源还被引用。2.5 多重catch的匹配顺序子类在前父类在后多个 catch 块并存时异常匹配是从上到下的顺序。Java 不会为你选最合适的 catch它只按书写顺序找第一个能接住这个异常类型的块。所以如果父类 Exception 写在子类 IOException 前面IOException 的 catch 块永远到不了编译器会直接报错该异常已经被父类 catch 捕获。Java 7 引入的多异常捕获语法也常被追问try { // 读写文件、解析格式 } catch (IOException | ParseException e) { // 统一处理e日志输出 }注意这里 catch 参数 e 的隐式类型是这些异常的公共父类而且 e 被隐式声明为 final不能在 catch 块里重新赋值。这个细节很多人答不上来。为什么要多个 catch一个复杂方法可能面临网络异常、解析异常、业务校验异常同时存在拆分 catch 块能让你针对不同情况进行差异化处理而用多异常捕获则是让处理逻辑相同时避免重复代码。3. 异常设计原则从懂语法到会设计的5道题3.1 什么时候该捕获什么时候该抛出一条判据走天下我面试时经常给出一道场景题你在 service 层调用 dao 层dao 层抛了个 SQLException你该怎么办很多候选人第一反应是 try-catch 包起来然后打印日志这恰恰是最需要纠正的习惯。判断该捕获还是该抛出的关键是看当前层能不能对异常做出有意义的补偿。如果当前层有降级方案、可以重试、能返回兜底数据那就捕获如果当前层什么都做不了就应该向上抛把选择权交给更上层。service 层面对 SQLException 时常见的正确处理是转化为自己的业务异常再抛出去或者回滚事务后抛给 controller而不是默默吞掉然后返回 null 告诉上面我没出错。可以从分层视角拆开说DAO 层一般不适合直接处理异常向上抛或转成数据访问异常让 service 层决定是否重试或回滚。Service 层可以处理与业务状态相关的异常做补偿、重试、回滚等。Controller/API 层捕获通用异常转换成统一的响应结构返回给客户端。答案里如果能强调异常处理的原则不是消除异常而是把异常放在最合适的一层去应对面试官会觉得你有工程判断力。3.2 自定义异常命名、继承、构造函数、错误码自定义异常是设计题的重灾区。很多人知道要继承 RuntimeException但构造函数不完整导致后续排错缺信息。一个合格的自定义异常至少要考虑四件事。第一继承谁。业务系统中我强烈建议继承 RuntimeException因为受检异常会污染调用链但如果你的异常需要强制调用方感知处理比如参数校验异常可以继承 Exception。第二命名规范。以 Exception 结尾比如 OrderException、PayException千万不要叫 OrderError 之类。第三构造函数完整。最好覆盖无参、带 message、带 message 和 cause 三个构造。有时还要提供带 errorCode 的版本。第四是否携带更多上下文。业务异常最好带 errorCode方便前端展示对应文案和产品侧定位。下面是一个我常用的模板public class OrderException extends RuntimeException { private final int errorCode; public OrderException(int errorCode, String message) { super(message); this.errorCode errorCode; } public OrderException(int errorCode, String message, Throwable cause) { super(message, cause); this.errorCode errorCode; } public int getErrorCode() { return errorCode; } }这样做的好处是全局异常处理器捕获到 OrderException 后可以直接取出 errorCode 返回给前端不需要再通过字符串解析业务错误。平时写业务代码时自定义异常别乱造一个模块里有三五个稳定的异常类型就够了不要每个方法都 new 一个异常类。3.3 异常日志把堆栈和上下文一起打出来才叫有效异常日志是我在实际项目中复查最多的点。很多线上问题排查困难不是因为难而是日志里只有 e.getMessage()连堆栈都没有。你说一个 NullPointerException 出现在哪一行都不知道排查只能靠猜。正确做法是把整个异常对象传给日志框架而不是只传 message。用日志框架的写法是// 不推荐只打印message丢失堆栈 log.error(处理失败 e.getMessage()); // 推荐传入异常对象自动记录堆栈 log.error(订单{}处理失败订单状态{}, orderId, status, e);第二个写法的好处有两个一是堆栈完整能定位到具体抛出点二是上下文参数能帮你理解这个异常发生在什么业务背景下。日志里最忌讳的就是只有报错文案没有订单号、没有用户标识、没有调用来源这种日志在排查时基本报废。还有一个重复记录的问题。很多团队在 DAO 层记一次service 层又记一次controller 层再记一次一个异常出现三份日志看起来信息很全实际上分散了排查注意力而且不同层记录时间可能有细微差异反而干扰判断。我的做法是在最终处理异常的那一层记一次完整日志中间层做异常包装时可以简记或不记保持日志链路干净。3.4 避免空指针从防御式写法到Optional的正确姿势NullPointerException 一出现Java 面试的气氛瞬间就活了。面试官想看的不是你背了多少判空方法而是你有没有形成系统的空值防护意识。日常代码里最常用的防护手段是提前判断比如方法入口校验参数public void process(Order order) { if (order null) { throw new IllegalArgumentException(order不能为空); } if (order.getId() null) { throw new IllegalArgumentException(order.id不能为空); } // 业务处理 }Java 8 之后 Optional 也很流行但用错的人很多。Optional 适合用作返回值表达可能没有值它不适合用来做字段类型也不适合作为方法参数传进来——把 Optional 传进方法意味着你又得判断一次外层 Optional 是否为空反而复杂化了。有一个比较新的点也可以提JDK 14 之后 JVM 对空指针有更友好的诊断信息出现 NPE 时能直接提示是哪个字段为 null而不是笼统地抛一个堆栈。面试时提到Helpful NullPointerException能显得你持续在跟进版本演进很加分。另外Objects.requireNonNull 是一个被低估的利器它可以在方法入参校验时快速抛出 NPE比手写 if throw 简洁而且语义清晰。3.5 滥用异常做业务分支为什么会成为面试反面教材有一类代码很能暴露程序员水平就是拿异常当正常的业务判断用。典型情况是把整个方法用 try-catch 包住通过 catch 某个特定异常来推出循环或者设置默认值。这样做的大问题有三个。第一是性能。异常创建需要填充堆栈这是一次栈帧遍历成本远高于普通 if 判断如果循环每轮都走异常分支性能影响可能非常显著。第二是可读性。异常名本质上代表非常规状态它混入正常流程会让阅读者无法判断哪些情况是预期哪些是真错误。第三是安全。一旦 catch 块写得不严谨真正的错误可能被当作正常业务吞掉。正确的业务分支做法是预期内的状态用 if 判断或 switch 分支预期外的错误用异常表达。比如根据订单状态决定下一步是分支逻辑应该用枚举加 switch外部接口响应异常才是异常逻辑应该抛出去或按降级策略处理。举一个反例// 反例把数组边界当成循环结束信号 try { while (true) { System.out.println(list.get(i)); } } catch (IndexOutOfBoundsException e) { // 用异常结束循环 }这种写法如果在面试题里出现基本就是考察你对异常只用于非常规状态的理解。能一眼看出问题并且解释清原因的人才算是真正理解异常设计的本质。4. 异常高级场景拉开面试差距的5道题4.1 异常的性能代价别用异常控制流程根源在堆栈填充这道题往往藏在一个更宽泛的问题里为什么说不要用异常处理正常流程性能是最核心的技术原因。异常对象创建时JVM 会调用 fillInStackTrace()这个方法会遍历当前线程的调用栈把每一层栈帧记录下来。调用栈越深遍历成本越高而且这个过程是 JVM 原生的异常抛出频率一旦高起来对系统吞吐的影响非常明显。我用一个简单测试验证过普通的 for 循环加 if 判断和同样循环里故意抛异常再捕获吞吐差距能到几十倍以上异常方式甚至可能把机器打满。这个数据背后的原理就是 fillInStackTrace 的栈遍历和对象分配开销。不过在特定场景下如果你确实需要一个异常对象但又完全不需要堆栈信息比如某些自定义的轻量级业务状态信号可以覆盖 fillInStackTrace 让异常创建成本大幅下降public class LightweightStatusException extends RuntimeException { Override public synchronized Throwable fillInStackTrace() { return this; // 不填充堆栈加快异常创建 } }这个技巧面试时能讲出来会非常亮眼。但它也带来一个代价异常对象没有堆栈排错完全靠 message 和上下文日志除非你对场景非常有把握否则不建议在生产大量使用。4.2 线程里的异常run方法签名上没有throws怎么办Runnable 的 run() 方法没有声明 throws所以受检异常根本抛不出去只能在 run 内部 try-catch。很多人在这里的认知是线程出异常会直接挂掉对但不够准确。如果线程没有被自定义的 UncaughtExceptionHandler 接管默认处理逻辑是把异常堆栈打印到 System.err然后线程结束。对于线程池里的工作线程来说是线程结束而不是整个线程池挂掉。面试常给的一道进阶题是把任务提交给 ThreadPoolExecutor任务内部抛出 RuntimeException调用方怎么感知这要区分提交方式。如果用 execute() 提交异常默认会被线程池内部吞掉外部拿不到如果用 submit() 返回 Future异常会被封装成 ExecutionException在调用 future.get() 时抛出。ExecutorService pool Executors.newFixedThreadPool(2); Future? future pool.submit(() - { throw new RuntimeException(任务内部失败); }); try { future.get(); } catch (ExecutionException e) { Throwable cause e.getCause(); // 真正的任务异常 System.out.println(捕获任务异常 cause.getMessage()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }这个写法里通过 ExecutionException 拿到 cause 才能看到真正异常直接捕获到的是包装后的异常。能讲清楚 execute、submit、Future.get 这三种异常感知路径的人说明对线程池有实操经验不是只看过文档。4.3 异常丢失为什么日志里明明该有异常却什么都没有异常丢失是生产事故里最隐蔽的坑。最典型的三条路线第一条是空 catch 块catch 住了然后什么都不写。这个不用多解释属于严重违规。第二条是 finally 里抛异常覆盖原始异常前面提过。第三条是 try-with-resources 出现之前的古老写法try 块抛异常 Afinally 里 close 抛异常 BB 直接把 A 覆盖导致你看到的异常是关文件失败而真正问题可能是文件解析失败。Java 7 之后这个问题被治本了try-with-resources 会把 close 时发生的异常作为 suppressed 附加到主异常上StackTrace 里会显示 Suppressed: 。如果你接手的老项目还在用 finally close建议逐步改成 try-with-resources。排查异常丢失时我有一个习惯先看日志里有没有空 catch再看 finally 里有没有可能抛异常的代码最后查有没有 catch 里 return 不打印日志。按这个顺序排查绝大多数异常神秘消失都能找到根源。这条经验在面试里讲出来比单纯背诵知识点更有说服力。4.4 反射、Stream、Lambda里的异常三个高频陷阱反射调用是异常处理的隐藏雷区。Method.invoke() 如果被调方法抛异常反射框架会把真实异常包装成 InvocationTargetException 抛出来。你直接 catch 反射相关异常时拿到的可能是包装壳真正原因藏在 cause 下面。我见过不止一个人 catch 到 InvocationTargetException 后打印了一下以为看到了原因其实只看到了壳。try { Method method obj.getClass().getMethod(doSomething); Object result method.invoke(obj); } catch (InvocationTargetException e) { Throwable realCause e.getCause(); // 这才是真正异常 log.error(业务方法调用失败, realCause); }Stream API 和 Lambda 里的受检异常处理是另一个常见问题。函数式接口的方法签名都没有 throws 声明所以你在 Lambda 里直接抛受检异常会编译不过。常见做法有三种一是包成 RuntimeException二是自己定义一个允许受检异常的函数式接口三是把流操作拆成传统 for 循环。我推荐第二种在项目公共工具类里定义这样的接口并不复杂FunctionalInterface public interface ThrowingConsumerT { void accept(T t) throws Exception; } public static T ConsumerT unchecked(ThrowingConsumerT throwingConsumer) { return t - { try { throwingConsumer.accept(t); } catch (Exception e) { throw new RuntimeException(e); } }; }有了这个工具Stream 里的代码就可以保持简洁同时异常又不会静默丢失。面试时能顺势讲出这种封装思路说明你不是第一次被 Lambda 异常坑过的人。4.5 异常链与initCause业务异常也要保留根因异常链是包装异常场景的技术底座。简单说当你在 catch 到一个底层异常后准备向上抛一个业务异常这个时候一定要把底层异常作为 cause 传进去。这样排错时可以通过内层的 Caused by一步不差地追到最原始的故障点。有两种写法// 方式一构造方法传cause throw new OrderException(1001, 订单创建失败, e); // 方式二创建后调用initCause OrderException oe new OrderException(1001, 订单创建失败); oe.initCause(e); throw oe;需要注意的是 initCause 只能被调用一次而且如果构造时已经传过 cause再调 initCause 会抛 IllegalStateException。为什么要有 initCause 而不是完全靠构造方法因为是给那些构造方法无法预知 cause 的场景留的口子比如某些代理生成、反序列化的场景。如果你能把这个为什么有 initCause讲明白面试官基本能确认你研究过异常源码的细节。关于异常链还有一个常考点printStackTrace 打印出的形式是 Caused by说明 JVM 在递归遍历 cause 链。当你做异常包装时一定要把 getCause 链走完不要只看最外层异常。很多线上问题定位慢就是只看第一行堆栈没往下翻 Caused by。5. 面试现场的实际体感最后再说几句带人这些年我面试过的候选人少说也上百了。我最直观的感受是异常题能不能答出层次基本能判断一个人平时写代码有没有思考的习惯。背下这20道经典面试题只是第一步更重要的是每道题背后的设计理由和工程选择。真正让人印象深刻的候选人往往会在回答里加一句这个坑我在生产环境踩过……哪怕讲的是个很小的例子也比干巴巴背课本强得多。最后分享一个小建议准备 Java 异常面试时别只坐在椅子上看面经。花一个下午把这20道题涉及的代码挨个写一遍用 debugger 看执行流程亲手验证 finally 覆盖返回值、异常链的 Caused by、try-with-resources 的 suppressed。眼见为实之后你对异常机制的理解会进入到另一个层次面试时也能讲得更从容。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表