5996 字
12 分钟
博客学习日志
博客日志:后端研发深度复盘与面试集锦
一、 Java 进阶:从语法到实战
基础概念篇
1. 你在简历中提到熟悉异常处理,请解释 Error 和 Exception 的区别。
解答:
- Error(错误): 通常是程序无法处理的严重问题,如
OutOfMemoryError。它们通常与 JVM 运行状态有关,不建议在代码中通过try-catch捕获。 - Exception(异常): 程序可以处理的情况。分为:
- 编译时异常 (Checked): 必须手动处理,否则无法编译。
- 运行时异常 (Unchecked): 如
NullPointerException,通常可以通过良好的编程习惯避免。
2. 什么是 Java 的 Stream API?它在处理项目数据(如人员得分)时有什么优势?
解答:
- 定义: Stream 是 Java 8 引入的处理集合数据的函数式编程方式。
- 优势: 在“科研人员画像系统”中计算人员综合得分时,通过 Stream API 可以极其简洁地完成数据的过滤 (filter)、映射 (map) 和 聚合 (reduce),大大减少了冗余的
for循环代码,提高了代码的可读性。
3. 谈谈你对 Java 泛型中类型擦除的理解。
解答:
- 定义: 泛型是 Java 5 引入的特性,但在编译阶段有效。编译后的字节码中,泛型类型会被擦除,替换为原始类型(如
List<T>变成List),并自动插入强制类型转换。 - 意义: 保证与旧版本 JVM 的向后兼容性。
- 局限: 运行时无法获取泛型的具体类型,例如
new T()是不允许的,也不能用instanceof检查泛型类型。
4. Java 中的 == 和 equals() 有什么区别?
解答:
==运算符: 比较基本数据类型时,比较的是值;比较引用类型时,比较的是内存地址。equals()方法: 是Object类的方法,默认行为也是比较内存地址。但通常被重写(如String、Integer类)来比较对象的内容是否相等。- 使用原则: 比较对象内容时,应重写
equals()并同时重写hashCode()方法。
5. 什么是 Java 的反射机制?你在项目中如何使用它?
解答:
- 定义: 反射是指在运行状态中,对于任意一个类,都能够获取其所有属性和方法;对于任意一个对象,都能调用其任意方法和属性。
- 项目应用:
- 动态代理: 在 Spring Security 中,利用反射实现 AOP,动态增强权限校验逻辑。
- 框架配置: 在项目初始化时,通过反射读取注解信息,动态注册 Bean 或生成 SQL 语句。
集合框架篇
6. 请对比 HashMap 和 Hashtable 的区别。
解答:
- 线程安全:
Hashtable是线程安全的(方法用synchronized修饰),HashMap非线程安全。 - 性能:
HashMap性能更高,单线程环境下优先使用。 - Null 值:
HashMap允许一个 null 键和多个 null 值;Hashtable不允许 null 键或 null 值。 - 扩容机制:
HashMap初始容量 16,扩容为 2 倍;Hashtable初始容量 11,扩容为 2 倍+1。
7. ConcurrentHashMap 是如何实现线程安全的?
解答:
- JDK 1.7: 采用分段锁(Segment),将数据分成多个段,每个段独立加锁,提高并发度。
- JDK 1.8+: 改用
synchronized+ CAS(乐观锁)锁住链表或红黑树的头节点,粒度更细,并发性能更好。 - 核心特性: 读操作基本无锁,写操作只锁住当前操作的桶,支持高并发场景。
8. ArrayList 和 LinkedList 分别适用于什么场景?
解答:
ArrayList: 基于动态数组实现。适合随机访问(get和set)频繁的场景,时间复杂度 O(1);但在中间插入/删除元素效率低,因为需要移动元素。LinkedList: 基于双向链表实现。适合频繁插入/删除(尤其是头部和尾部)的场景,时间复杂度 O(1);但随机访问效率低,需要遍历。
9. 如何保证遍历集合时不被其他线程修改?
解答:
- Fail-Fast 机制: 使用
java.util包下的集合(如ArrayList、HashMap),当多个线程并发修改时,会抛出ConcurrentModificationException。 - Fail-Safe 机制: 使用
java.util.concurrent包下的集合(如CopyOnWriteArrayList、ConcurrentHashMap),它们通过复制原数据或分段锁来保证遍历时不被修改,不会抛出异常。
并发编程篇
10. synchronized 和 ReentrantLock 有什么区别?
解答:
- 使用方式:
synchronized是关键字,自动加解锁;ReentrantLock是 API 类,需要手动lock()和unlock()。 - 功能特性:
ReentrantLock提供了更多高级功能,如公平锁、可中断锁、超时获取锁、多个 Condition。 - 性能: 早期
synchronized性能较差,但现在 JVM 对两者做了优化,性能差异不大。建议简单场景用synchronized,复杂并发场景用ReentrantLock。
11. 什么是 volatile 关键字?它能保证原子性吗?
解答:
- 作用:
- 可见性: 保证一个线程修改变量后,其他线程能立即看到最新值(禁止线程本地缓存)。
- 有序性: 禁止指令重排序优化(如 DCL 单例模式中必须用
volatile)。
- 原子性:
volatile不能保证原子性。例如count++操作,即使变量被volatile修饰,多线程下仍会数据错乱,需用synchronized或AtomicInteger保证原子性。
12. 说说线程池的核心参数(ThreadPoolExecutor)。
解答:
- corePoolSize: 核心线程数,即使空闲也会保留。
- maximumPoolSize: 最大线程数。
- keepAliveTime: 非核心线程空闲存活时间。
- workQueue: 任务等待队列(如
LinkedBlockingQueue、SynchronousQueue)。 - threadFactory: 创建线程的工厂(可自定义线程名)。
- handler: 拒绝策略(如
AbortPolicy抛出异常、CallerRunsPolicy让提交线程自己执行)。
13. 如何理解 Java 内存模型(JMM)?
解答:
- 定义: JMM 定义了多线程之间共享变量的可见性、原子性和有序性规则。
- 核心概念:
- 主内存: 所有变量都存储在主内存中。
- 工作内存: 每个线程有自己的工作内存,保存主内存变量的副本。
- 规则: 线程对变量的所有操作都必须在工作内存中进行,不能直接操作主内存;不同线程之间无法直接访问对方的工作内存,必须通过主内存传递。
JVM 与性能优化篇
14. 请描述 JVM 的内存区域划分。
解答:
- 线程私有:
- 程序计数器: 当前线程执行的字节码行号指示器。
- 虚拟机栈: 存储局部变量表、操作数栈、方法出口等,每个方法调用对应一个栈帧入栈出栈。
- 本地方法栈: 为 native 方法服务。
- 线程共享:
- 堆: 存放对象实例,垃圾回收的主要区域(分新生代、老年代)。
- 方法区: 存储类信息、常量、静态变量、即时编译后的代码(元空间/Metaspace)。
15. 你遇到过哪些 OOM(OutOfMemoryError)场景?如何排查?
解答:
- 常见场景:
- 堆溢出: 对象过多且无法回收(死循环创建对象、内存泄漏)。排查用
jmap或 MAT 分析堆转储文件。 - 栈溢出: 递归调用过深或线程数过多。检查递归逻辑或线程池配置。
- 元空间溢出: 加载类过多(热部署、反射生成类)。检查是否有类加载器泄漏。
- 堆溢出: 对象过多且无法回收(死循环创建对象、内存泄漏)。排查用
- 排查步骤:
- 添加 JVM 参数
-XX:+HeapDumpOnOutOfMemoryError自动生成堆转储。 - 用 MAT 或 JProfiler 分析大对象引用链。
- 结合业务代码,定位内存泄漏点(如未关闭的资源、不正确的缓存)。
- 添加 JVM 参数
新特性与编码实践
16. Java 8 的 Optional 类有什么作用?如何使用?
解答:
- 作用: 主要解决
NullPointerException问题,提供一种更优雅的方式来处理可能为 null 的值。 - 常用方法:
ofNullable():创建可能为空的 Optional。orElse()/orElseGet():值为空时返回默认值。map()/flatMap():对值进行转换。ifPresent():值存在时执行操作。
- 示例:
Optional.ofNullable(user).map(User::getAddress).orElse("默认地址"),避免了多层 null 检查。
17. 什么是方法引用(Method Reference)?有哪些类型?
解答:
- 定义: 方法引用是 Lambda 表达式的简化写法,用
::操作符表示。 - 类型:
- 静态方法引用:
类名::静态方法名(如Math::max) - 实例方法引用:
对象::实例方法名 - 特定类型任意对象方法引用:
类名::实例方法名(如String::length) - 构造方法引用:
类名::new
- 静态方法引用:
18. 如何写出更优雅的 Java 代码?(编码规范与最佳实践)
解答:
- 遵循命名规范: 类名大驼峰,方法/变量小驼峰,常量全大写。
- 合理使用 Optional: 避免返回 null,减少 NPE 风险。
- 善用 Stream API: 替代复杂的循环和条件判断。
- 接口设计原则: 面向接口编程而非实现类。
- 异常处理: 不要捕获了异常什么都不做,至少打印日志。
- 单测覆盖: 关键业务逻辑必须有单元测试(JUnit + Mockito)。
- 代码复用: 提取公用方法,避免重复代码(DRY 原则)。
19. 什么是 Java 的 SPI 机制?
解答:
- 定义: SPI(Service Provider Interface)是 JDK 内置的服务提供发现机制,用于实现框架的扩展性。
- 工作原理: 在
META-INF/services目录下创建以接口全限定名命名的文件,内容为实现类的全限定名。通过ServiceLoader加载实现类。 - 项目应用: 在开发 SDK 或框架时,利用 SPI 允许第三方扩展功能(如 JDBC 驱动加载、Spring 的
spring.factories)。
20. 谈谈你对“面向接口编程”的理解,并结合项目说明。
解答:
- 理解: 面向接口编程意味着调用者只依赖接口定义,而不依赖具体实现类。这降低了代码耦合度,提高了可扩展性和可测试性。
- 项目应用: 在“科研人员画像系统”中,我定义了
ScoreCalculator接口,然后分别实现PaperScoreCalculator(论文得分)、ProjectScoreCalculator(项目得分)等。当新增得分规则时,只需新增实现类,无需修改调用方的业务逻辑,符合开闭原则。
21. 什么是 Java 的 Stream API?它在处理项目数据(如人员得分)时有什么优势?
解答:
- 定义: Stream 是 Java 8 引入的处理集合数据的函数式编程方式。
- 优势: 在“科研人员画像系统”中计算人员综合得分时,通过 Stream API 可以极其简洁地完成数据的过滤 (filter)、映射 (map) 和 聚合 (reduce),大大减少了冗余的
for循环代码,提高了代码的可读性。
部分信息可能已经过时









