导读:下篇开始从“写得出来”走向“为什么框架这样设计”。异常、泛型、反射、动态代理、SPI、序列化和 I/O,基本就是 Java 工程化面试的入口。
系列阅读:上篇:语法、数据类型与方法 | 中篇:面向对象、Object 与 String
一、异常体系先记一棵树,而不是记一堆类名
所有异常和错误最终都继承自 Throwable。往下最关键的分叉只有两条:Exception 和 Error。
| 类型 | 典型含义 | 处理策略 |
|---|---|---|
| Exception | 程序可感知、可处理的问题 | 按业务或调用边界处理 |
| Error | 更严重的系统级故障 | 一般不建议在业务代码里兜底捕获 |
Checked Exception 和 Unchecked Exception 的区别也最好顺着这个体系答。编译期强制处理的是受检异常,典型如 IOException、SQLException;RuntimeException 及其子类属于非受检异常,更常代表程序 Bug 或调用不当。
如果面试官问你的偏好,可以直接说:默认更倾向于 Unchecked Exception,只有当异常本身就是业务流程的一部分、并且调用方必须处理时,才会考虑 Checked Exception。
二、try-catch-finally 会写不难,难在知道边界
finally 通常用于资源释放和收尾逻辑,但它并不是“100% 一定执行”。比如 System.exit 直接终止 JVM、线程非正常死亡等情况,finally 就可能执行不到。
另外一个高频坑点是:不要在 finally 里写 return。因为 finally 中的 return 会覆盖 try 或 catch 中本来准备返回的结果,让代码行为变得反直觉,也更难排查。
try (BufferedReader reader = Files.newBufferedReader(path)) {
return reader.readLine();
} catch (IOException e) {
throw new IllegalStateException("读取配置失败", e);
}
凡是实现了 AutoCloseable 或 Closeable 的资源,都优先考虑 try-with-resources。这种写法更短,也更不容易漏掉 close。
三、异常设计真正拉开差距的,是这几个工程习惯
- 不要复用同一个异常对象,异常栈信息应当反映当前抛出的现场。
- 异常信息要能直接帮助定位问题,而不是只写一个“出错了”。
- 优先抛更具体的异常类型,比如 NumberFormatException 比 IllegalArgumentException 更有定位价值。
- 避免重复记录日志,尤其是在同一个调用链里层层 catch 再层层 error。
四、泛型解决的核心问题,是类型安全和可复用
泛型最直接的价值就是让错误尽量提前到编译期。没有泛型时,集合里拿出来的是 Object,需要手动强转;用了泛型之后,类型约束会更明确,IDE 和编译器也能更早发现问题。
常见的泛型使用方式主要有三种:
- 泛型类,例如 CommonResult<T>。
- 泛型接口,例如某些转换器、处理器接口。
- 泛型方法,例如工具类里的通用方法。
项目里你最容易见到的落点,就是统一响应体、分页结果、导入导出工具类和各种集合工具方法。
五、为什么说反射是框架的基础设施
反射允许程序在运行时获取类的信息,动态地创建对象、调用方法、读取或修改字段。这种“编译期不知道,运行时再决定”的能力,正是很多框架能灵活运作的原因。
Spring、MyBatis、Hibernate 这些框架,本质上都大量依赖了反射:
- Spring 用它做 Bean 扫描、实例化和依赖注入。
- AOP 用它调用目标方法并织入增强逻辑。
- ORM 框架用它把数据库记录映射为 Java 对象。
反射的优点是灵活,缺点也同样明显:性能开销更高、破坏封装、可读性更差。因此业务代码里不应滥用,框架层才是它最合适的主战场。
六、动态代理为什么经常和 Spring AOP 一起出现
动态代理的目标很简单:不改原始业务代码,也能在方法调用前后织入额外逻辑,比如日志、事务、权限、监控。
| 方案 | 前提 | 实现方式 | 典型场景 |
|---|---|---|---|
| JDK 动态代理 | 目标类实现接口 | 生成接口代理对象 | Spring AOP 常见默认路径 |
| CGLIB | 目标类无需接口 | 生成目标类子类 | 没有接口时的增强方案 |
注意,CGLIB 因为基于继承,所以 final 类、final 方法、private 方法都不是它擅长处理的对象。
七、注解和 SPI,分别解决“声明规则”和“扩展实现”
注解可以理解为一种元信息。它本身不做事,但会给编译器或运行时框架提供“这里要特殊处理”的标记。像 @Override 是编译期检查,Spring 中的 @Component、@Value 则更多依赖运行时反射解析。
SPI 则是另一条思路:由调用方定义接口标准,让不同实现方按这个标准接入。JDBC 驱动、日志实现、Dubbo 扩展点,都是非常典型的 SPI 场景。
理解 SPI 和 API 的区别,最容易的一句话是:
- API:实现方定义接口,调用方来使用。
- SPI:调用方定义接口,实现方来适配。
八、序列化最该记住的不是定义,而是边界
序列化是把对象变成可存储、可传输的格式,反序列化则是把这些数据恢复成对象。典型场景包括 RPC、缓存、消息传递和对象持久化。
如果某个字段不想参与序列化,可以用 transient 修饰。它的含义是:这个字段不会被持久化,反序列化回来时会恢复成默认值。同时也别忘了,static 字段本来就不属于对象实例,自然也不会随着对象一起序列化。
至于为什么不推荐 JDK 原生序列化,原因通常有三个:跨语言差、性能一般、安全风险更高。所以实际工程里更常看到 Protobuf、Kryo、Hessian 这类方案。
九、I/O 的问题,最后通常会落到模型差异
Java I/O 从抽象结构上分为输入和输出、字节流和字符流。字节流适合处理任意二进制数据,字符流更适合处理文本,因为它已经帮你考虑了字符编码转换。
而真正的高频追问,往往是 BIO、NIO、AIO 的区别:
- BIO:同步阻塞,一个连接通常对应一个线程模型。
- NIO:同步非阻塞,适合高并发场景,核心是 Channel、Buffer、Selector。
- AIO:异步非阻塞,更多由操作系统回调通知结果。
如果只是普通业务开发,不一定天天直接写 NIO,但你最好能说清楚为什么 Netty 这类框架会建立在 NIO 思路上。
十、语法糖看起来轻,实际上能反映你对编译器的理解
所谓语法糖,就是为了让代码更好写、更好读,但最终会在编译阶段被“解糖”成 JVM 真正能理解的基础形式。增强 for、自动装箱拆箱、可变参数、lambda、try-with-resources,这些都属于 Java 里的常见语法糖。
所以,当你说“Java 支持某个语法”时,最好顺手补一句:这是编译器层面的便利,不是 JVM 原生就直接认识这种高级写法。这会让你的回答更像理解过原理,而不是只记了表层用法。
十一、下篇最适合面试收尾的复盘顺序
- 先把异常体系和 try-with-resources 讲清楚。
- 再讲泛型解决了什么问题,以及项目里怎么用。
- 接着解释反射、动态代理、注解、SPI 为什么是框架基础。
- 最后用序列化、I/O、语法糖把工程化视角补齐。
一句话总结:下篇的价值在于把 Java 从“语言”推到“平台和生态”的层面。能把这些内容讲明白,通常就已经不只是初级语法视角了。