Android 面试里有一类题专门筛人,异常体系 Throwable 的原理追问就是典型:第一层问"Error 和 Exception 什么关系",第二层问"受检和非受检到底怎么分",第三层问"为什么 finally 保证执行、catch 吞异常有什么后果"。三层下来,背题的和理解的立刻分层。能随手画出那张继承树的人,八成是把异常处理用过、踩过、复盘过的。
先把结论放在前面:Throwable 下分 Error 与 Exception,Error 代表虚拟机层面不可恢复的问题(如 OOM),应用不应捕获;Exception 下分受检异常(Checked,强制调用方捕获或声明)与非受检异常(RuntimeException 及其子类,不强制)。这题要答好,光有结论不够,得能把"为什么这么设计"和"用错会怎样"一步步推出来。
机制拆解
这篇的答案可以很短,也可以很长——把它讲成有层次的长答案,面试官才会觉得你不是背的。
这些坑的正确绕法
最常见的坑是 catch 里只打印堆栈然后继续执行。错误被静默吞掉,业务流程带着错误状态往下走,问题在线上以更远、更难定位的形式爆发——比如返回了半截数据、写了脏缓存。更糟的是,吞掉异常后上层完全无从感知,监控也抓不到,等到用户投诉时链路早就断了。正确的做法是:能处理的就地处理并给出兜底,不能处理的要么抛出去,要么包装成更上层的异常上抛,绝不停留在"打印一下"。
其次是拿异常当流程控制手段。比如在循环里用抛出异常来跳出多层嵌套,或用异常判断"元素是否存在"。异常对象的创建要填充完整的堆栈轨迹,这个开销在热点路径上足以把性能打穿。判断存在用 contains、跳出用标志位或 break,本就是为该场景设计的低成本手段,不该让异常来兼职。
还有一个更隐蔽的坑:在 finally 里抛新异常,会盖掉 try 块里的主异常。主异常的信息就此丢失,排查时只能看到 finally 的错误,真正的根因被埋了。Java 7 之后提供了 addSuppressed 机制来保留被压制的主异常,前提是你用了 try-with-resources 或手动调用 addSuppressed,手写 finally 很容易踩这个坑。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
// 受检异常必须声明或捕获;非受检不强制
try {
throw new IOException("read failed"); // Checked:必须声明或捕获
} catch (IOException e) {
throw new RuntimeException(e); // 包装成 Unchecked 上抛,保留原因
} finally {
closeQuietly(); // 即便 try/catch 抛异常,这里也执行
}
// Error(如 OutOfMemoryError)不应被捕获处理
// RuntimeException(如 NullPointerException)不强制声明,属于编程错误
这段代码值得盯三处:第一处,受检异常 IOException 必须被捕获或声明 throws,否则编译不过,这是 Java 强制调用方"正视"可恢复错误的方式;第二处,把受检异常包装成 RuntimeException 上抛,既满足接口约束又不丢根因(cause 链完整);第三处,finally 无论成败都会执行(除 System.exit),适合做资源清理,但里面抛异常会吞掉主异常,要小心。面试讲到这一层,基本就稳了。
这题在面试里怎么问、怎么答
"请简单介绍一下异常体系 Throwable,它在 Android 开发中起什么作用?"按"是什么 → 干什么用 → 在项目里怎么用"递进介绍,全程不超过三分钟。如果是网络、IO 这类外部不确定性操作,用受检异常或明确的业务异常逼迫调用方处理;如果是空指针、越界这类编程错误,属于 RuntimeException,靠代码质量而非异常捕获来规避。如果要把异常处理沉淀成团队规范,建议加两条硬约定:一是严禁 catch 后只打印不处理或吞掉,二是业务异常必须带可定位的上下文(订单号、用户 ID 等)。
"异常体系的底层原理是什么?能不能详细说一下?"先讲设计动机:受检异常是 Java 在编译期把"你考虑过这个失败了吗"写进类型系统,非受检异常是给真正不该发生的编程错误留的口子。再拆内部机制:异常抛出时 JVM 会填充堆栈轨迹(fillInStackTrace),这涉及遍历当前调用栈,开销可观,因此异常不可用于常态路径。机制别空讲,配核心片段最稳——包装成 unchecked 上抛时记得把原异常作为 cause 传入,否则根因链断裂。如果讲给刚入职的新人,从一个"catch 吞异常导致线上脏数据"的反例切入,比讲定义记得牢。
"在使用异常体系时遇到过什么问题?"讲真实案例:现象、定位、修复、验证,数字化的修复效果最加分。比如某次线上偶发数据错乱,最终定位到是某处 catch 了异常却继续返回了默认值,导致上游以为成功。修复是改成上抛并加监控,之后同类问题再没漏过。讲这类经历用"场景、行动、结果"三段式,逻辑顺、有数字最加分。
"异常体系和相关替代方案相比,有什么优劣?"考的是知识广度加选型能力,答案要有全景图。与"返回错误码"相比,异常能把错误从正常返回路径里分离出来、强制处理、且带堆栈便于定位,代价是性能与复杂度;与"Optional/Result 类型"相比,后者把失败显式化、不依赖堆栈、无性能开销,代价是要全员遵守约定。性能、易用性、生态、成本,选型就按这四项来,没有万能答案:能说清什么场景用什么,才叫真懂。选型时要把"用异常控制循环退出,堆栈填充开销把性能打穿"这类代价摆到台面上,再决定是否引入。
顺带提一个常被忽略的成本维度:异常不是免费的,但它贵在创建那一刻。fillInStackTrace 要遍历当前线程的完整调用栈,栈越深、调用越频繁,开销越明显。因此异常只该出现在"真正异常"的路径上,常态的成功路径不该依赖它传递控制流,否则性能会悄悄劣化。另一点是 Throwable 的 cause 链可以很长,打印日志时需要打印完整堆栈(含 suppressed 和 cause),只打印 getMessage 会丢根因。在 Android 里,主线程的异常会直接触发崩溃或 ANR,因此关键业务路径要就近捕获可恢复的异常、把不可恢复的异常带上上下文再上报,而不是让它在某个不相关的地方以一种难以排查的形式爆发。
给正在准备面试的你
把异常体系的要点放进一次真实的编码练习,检验自己是否真的掌握。比如写一个带资源清理的文件处理,分别用 try-finally 和 try-with-resources 实现,对比两者在"关闭也抛异常"时主异常是否保留。面试中关于异常体系的问题,关键在于能够从原理、应用、踩坑三个层面给出有深度的回答。
再补工程案例与踩坑——应用落点是梳理项目里三类异常的处理策略:可恢复(重试或降级)、可忽略(记录日志后继续)、必须上抛(带上下文),并给出代码骨架。把异常分级处理,比"一把梭 catch Exception"专业得多。
复习时别孤立刷题:LinkedList 与 ArrayList 选型——随机访问与插删的权衡这类题面试官爱连着问,先把边界理清。
划两句重点,别的都可以忘:Throwable 下分 Error 与 Exception,Error 代表虚拟机级不可恢复,应用不应捕获;受检异常强制处理,RuntimeException 不强制,但两者都讲究"别吞、别滥用"。
下一篇聊 try-catch-finally 与 try-with-resources:资源释放正确姿势——沿着今天这条主线继续往前走。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:==-与-equals-的区别:从栈堆内存说起
下一篇预告:try-catch-finally-与-try-with-resources:资源释放正确姿势
有任何问题欢迎在评论区留言交流。