Java 基础常见知识点与面试题总结(下):异常、泛型、反射与 I/O

下篇不再停留在语法层,开始进入框架底层常见的异常、泛型、反射、代理、序列化和 I/O 话题。

导读:下篇开始从“写得出来”走向“为什么框架这样设计”。异常、泛型、反射、动态代理、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-with-resources 的推荐写法
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 和编译器也能更早发现问题。

常见的泛型使用方式主要有三种:

  1. 泛型类,例如 CommonResult<T>。
  2. 泛型接口,例如某些转换器、处理器接口。
  3. 泛型方法,例如工具类里的通用方法。

项目里你最容易见到的落点,就是统一响应体、分页结果、导入导出工具类和各种集合工具方法。

五、为什么说反射是框架的基础设施

反射允许程序在运行时获取类的信息,动态地创建对象、调用方法、读取或修改字段。这种“编译期不知道,运行时再决定”的能力,正是很多框架能灵活运作的原因。

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 原生就直接认识这种高级写法。这会让你的回答更像理解过原理,而不是只记了表层用法。

十一、下篇最适合面试收尾的复盘顺序

  1. 先把异常体系和 try-with-resources 讲清楚。
  2. 再讲泛型解决了什么问题,以及项目里怎么用。
  3. 接着解释反射、动态代理、注解、SPI 为什么是框架基础。
  4. 最后用序列化、I/O、语法糖把工程化视角补齐。

一句话总结:下篇的价值在于把 Java 从“语言”推到“平台和生态”的层面。能把这些内容讲明白,通常就已经不只是初级语法视角了。

返回系列:上篇中篇

后浪云移动端信息流广告 后浪云主机服务 适合长期部署、独立站和海外机房需求。