Java基础常见面试题总结(下)
背诵重点
下篇保留偏框架底层和进阶基础的高频题:异常、泛型、反射、代理、注解、SPI、序列化、IO 和语法糖。这些题不要只背定义,必须能说出应用场景。
建议按这条线背:
- 异常:体系、Checked/Unchecked、
finally、try-with-resources。 - 泛型:作用、擦除、上下界。
- 框架底层:反射、代理、注解、SPI。
- 数据传输:序列化、IO、BIO/NIO/AIO。
Exception 和 Error 有什么区别?
Java 异常体系顶层是 Throwable,下面主要分为 Error 和 Exception。
Error 表示程序通常无法处理的严重问题,比如 OutOfMemoryError、StackOverflowError、NoClassDefFoundError。
Exception 表示程序可以处理的异常,又分为受检查异常和运行时异常。
面试回答:Error 一般不建议捕获,应该定位环境或 JVM 层面问题;Exception 才是业务代码重点处理对象。
Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception 是受检查异常,编译期必须 catch 或 throws,否则无法通过编译。常见例子:IOException、SQLException、ClassNotFoundException。
Unchecked Exception 通常指 RuntimeException 及其子类,编译期不强制处理。常见例子:NullPointerException、IllegalArgumentException、ClassCastException、ArithmeticException、ArrayIndexOutOfBoundsException。
实际建议:
- 业务可预期、调用方必须处理的异常,可以设计为 Checked Exception。
- 编程错误和参数错误更适合 Unchecked Exception,让问题暴露出来。
ClassNotFoundException 和 NoClassDefFoundError 的区别?
ClassNotFoundException 是异常,通常发生在反射、类加载器动态加载类时找不到目标类。
NoClassDefFoundError 是错误,通常表示编译时存在这个类,但运行时找不到,比如 jar 包缺失、依赖冲突、类初始化失败。
一句话背诵:前者是动态加载时找不到类,后者是运行时链接不到本该存在的类。
try-catch-finally 怎么执行?
try 用于包裹可能抛异常的代码,catch 用于捕获和处理异常,finally 通常用于释放资源。
即使 try 或 catch 中有 return,finally 通常也会在方法真正返回前执行。
不执行 finally 的典型情况:
- 调用
System.exit()退出 JVM。 - JVM 崩溃或进程被强杀。
- 当前线程死循环或永久阻塞,执行不到
finally。
不要在 finally 中写 return,它会覆盖前面的返回值或异常。
try-with-resources 有什么用?
try-with-resources 用于自动关闭资源,资源类必须实现 AutoCloseable 或 Closeable。
try (BufferedReader reader = new BufferedReader(new FileReader("a.txt"))) {
return reader.readLine();
}它比手写 finally 更安全,可以减少忘记关闭资源、关闭顺序错误、异常覆盖等问题。
常见场景:文件流、网络连接、数据库连接。
异常使用有哪些注意点?
高频注意点:
- 不要捕获异常后什么都不做。
- 不要用异常控制正常业务流程。
- 捕获异常要尽量具体,不要动不动
catch (Exception e)。 - 抛异常时保留原始异常信息,避免排查困难。
- 资源释放优先使用 try-with-resources。
- 日志不要重复打印,避免同一个异常被多层重复记录。
泛型有什么作用?
泛型把类型检查提前到编译期,提高类型安全,减少强制类型转换。
List<String> list = new ArrayList<>();
String value = list.get(0);如果没有泛型,集合里可以放任意对象,取出时需要强转,容易在运行期出现 ClassCastException。
泛型擦除是什么?
Java 泛型主要通过类型擦除实现。编译后,大多数泛型类型信息会被擦除成原始类型,必要时编译器插入强制类型转换。
例如 List<String> 和 List<Integer> 在运行时都主要表现为 List。
常见追问:
- 不能直接
new T(),因为运行时不知道T的具体类型。 - 不能直接创建泛型数组,因为数组运行时需要知道精确元素类型。
- 泛型类型不能用于
instanceof精确判断,比如不能判断list instanceof List<String>。
泛型上下界怎么理解?
上界用 extends,表示最多是某个类型或其子类,适合读取。
下界用 super,表示至少是某个类型或其父类,适合写入。
记忆口诀:PECS。
- Producer Extends:生产数据,用
? extends T。 - Consumer Super:消费数据,用
? super T。
List<? extends Number> readList;
List<? super Integer> writeList;反射是什么?
反射是在运行时获取类信息,并动态操作类、字段、方法、构造器的机制。
常见能力:
- 加载类:
Class.forName()。 - 创建对象:通过构造器实例化。
- 调用方法:通过
Method.invoke()。 - 操作字段:通过
Field读取或赋值。 - 读取注解:配合框架做元数据解析。
反射有什么优缺点?
优点:
- 动态灵活,编译期不知道具体类型也能操作对象。
- 适合框架做通用能力。
- 能配合注解、配置实现解耦。
缺点:
- 性能低于直接调用。
- 破坏封装,可能访问私有成员。
- 编译期类型检查弱,错误可能延迟到运行期。
- 在模块化和安全限制下可能需要额外配置。
优化思路:缓存 Class、Method、Field,减少重复查找;高频场景可考虑方法句柄或字节码增强。
反射有哪些应用场景?
典型场景:
- Spring 依赖注入和注解扫描。
- MyBatis 结果集映射。
- JSON 序列化和反序列化。
- JUnit 测试框架。
- 动态代理底层能力。
回答时最好结合框架:比如 MyBatis 查询结果后,通过反射把字段值设置到 Java 对象属性中。
静态代理和动态代理有什么区别?
静态代理是在编译期写好代理类,结构清晰,但每个目标类都要写代理类,维护成本高。
动态代理是在运行时生成代理对象,不需要手写大量代理类,更适合框架统一增强。
代理的核心价值:不修改业务代码的前提下增强方法,比如事务、日志、权限、监控、缓存。
JDK 动态代理和 CGLIB 动态代理有什么区别?
JDK 动态代理基于接口,目标类必须实现接口,核心是 InvocationHandler 和 Proxy.newProxyInstance()。
CGLIB 动态代理基于继承,通过生成目标类子类来增强方法,不要求接口,但不能代理 final 类和 final 方法。
Spring AOP 常见选择:
- 有接口时默认倾向 JDK 动态代理。
- 没有接口时使用 CGLIB。
动态代理在框架中有哪些场景?
常见场景:
- Spring AOP:事务、日志、权限、监控。
- MyBatis Mapper:接口没有实现类,但运行时生成代理对象执行 SQL。
- RPC 框架:调用本地接口方法,底层通过代理转换成远程调用。
一句话背诵:动态代理让框架可以在不侵入业务代码的情况下统一增强方法调用。
注解是什么?
注解是给代码添加元数据的机制,本身不直接改变业务逻辑,必须被编译器、运行时反射或框架解析后才有意义。
常见元注解:
@Target:注解可以用在哪里。@Retention:注解保留到哪个阶段。@Documented:是否生成到文档中。@Inherited:是否允许子类继承。
框架运行时读取注解,通常需要 @Retention(RetentionPolicy.RUNTIME)。
注解有哪些解析方式?
常见解析方式:
- 编译期处理:比如 Lombok、MapStruct、注解处理器。
- 类加载或运行期反射解析:比如 Spring 扫描
@Component、@Autowired。 - 字节码工具解析:比如 ASM、Javassist。
面试中重点讲运行期反射解析即可。
SPI 是什么?
SPI 是 Service Provider Interface,服务提供方接口。调用方定义接口和加载规则,具体实现由第三方提供。
JDK SPI 基本流程:
- 定义接口。
- 实现方提供实现类。
- 在
META-INF/services/接口全限定名文件中写实现类全限定名。 - 调用方通过
ServiceLoader.load()加载实现。
典型场景:JDBC 驱动加载、Dubbo 扩展点思想、日志框架适配。
SPI 和 API 有什么区别?
API 是服务提供给调用方使用的能力。
SPI 是框架提供接口,让第三方反过来扩展框架能力。
一句话背诵:API 是你调用别人,SPI 是别人按你的规则接进来。
SPI 有什么缺点?
JDK SPI 的缺点:
- 通常会一次性加载所有实现,不能很好地按需加载。
- 缺少依赖注入能力。
- 无法方便地按名称选择实现。
- 扩展能力较简单,复杂场景通常要框架自己封装。
Dubbo 等框架会在 JDK SPI 思想上扩展自定义 SPI。
序列化和反序列化是什么?
序列化是把对象转换成可存储或传输的数据形式。
反序列化是把数据还原成对象。
常见用途:
- 网络传输。
- 缓存存储。
- 持久化。
- RPC 调用。
常见协议:JSON、Protobuf、Hessian、Kryo、Avro、MessagePack。
transient 和 serialVersionUID 有什么用?
transient 用于标记字段不参与序列化。
static 字段属于类,不属于对象,普通对象序列化不会保存静态字段。
serialVersionUID 用于序列化版本兼容校验。反序列化时,如果字节流中的版本号和当前类版本号不一致,可能抛出 InvalidClassException。
为什么不推荐 JDK 原生序列化?
原因:
- 性能较差。
- 序列化后体积较大。
- 可读性差。
- 存在安全风险,反序列化漏洞影响很大。
- 跨语言能力弱。
实际项目里更常用 JSON、Protobuf、Kryo、Hessian 等方案。
Java IO 流怎么分类?
按数据单位分:
- 字节流:处理二进制数据,比如图片、音频、文件复制。
- 字符流:处理文本数据,涉及编码和解码。
按方向分:
- 输入流:读取数据。
- 输出流:写出数据。
字节流是基础,字符流本质上是在字节流基础上加入字符编码处理。
BIO、NIO、AIO 有什么区别?
BIO:同步阻塞 I/O,一个连接通常对应一个线程,模型简单,但高并发下线程成本高。
NIO:同步非阻塞 I/O,核心是 Buffer、Channel、Selector。一个线程可以通过 Selector 监听多个 Channel 事件,适合高并发网络编程。
AIO:异步非阻塞 I/O,由操作系统完成 I/O 后回调通知应用,Java 中使用相对较少。
一句话背诵:BIO 阻塞,NIO 多路复用,AIO 异步回调。
Channel、Buffer、Selector 分别是什么?
Channel 是双向数据通道,可以读也可以写。
Buffer 是缓冲区,数据读写都围绕 Buffer 进行。读数据时,从 Channel 读到 Buffer;写数据时,从 Buffer 写到 Channel。
Selector 是多路复用器,一个线程可以监听多个 Channel 的就绪事件,比如连接、读、写。
这三者是 Java NIO 的核心。
Java 中有哪些常见语法糖?
语法糖是编译器帮我们简化书写的语法,编译后会还原成更基础的结构。
常见语法糖:
- 自动装箱和拆箱。
- 增强
for循环。 - 泛型。
- 可变参数。
- 枚举。
- 内部类。
- Lambda 表达式。
- try-with-resources。
- 字符串
+拼接。 - switch 支持字符串。
面试重点是能说出一两个底层转换例子:比如增强 for 遍历集合会转成迭代器,自动装箱会调用 valueOf()。
下篇可以删除或只需了解的问题
这些内容不再单独展开:
Throwable的方法清单:知道getMessage()、printStackTrace()即可。- 泛型使用方式的大段示例:面试重点是类型安全、擦除、上下界。
- “项目中哪里用到了泛型”:结合自己项目回答,不需要文档长篇写。
- Java IO 设计模式清单:装饰器模式可以在 IO 专题里单独看。
- 语法糖完整清单:知道高频例子即可,不需要逐个展开。
下篇口述模板
回答框架底层题时按这个顺序:
- 先给定义。
- 再说底层机制。
- 补充优缺点。
- 最后给框架场景。
比如问“反射是什么”:
可以答:反射是在运行时获取类信息并操作字段、方法、构造器的机制。它让框架在编译期不知道具体类型的情况下动态创建对象、注入属性或调用方法。Spring 注入、MyBatis 映射、JSON 序列化都大量使用反射。缺点是性能低于直接调用,也会破坏封装,所以框架通常会缓存反射对象,减少重复查找。
