10559 字
21 分钟
博客学习日志#1
博客日志:后端研发深度复盘与面试集锦
一、 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() 有什么区别? [citation:5]
解答:
==运算符: 比较基本数据类型时,比较的是值;比较引用类型时,比较的是内存地址。equals()方法: 是Object类的方法,默认行为也是比较内存地址。但通常被重写(如String、Integer类)来比较对象的内容是否相等 [citation:5]。- 使用原则: 比较对象内容时,应重写
equals()并同时重写hashCode()方法。
5. 什么是 Java 的反射机制?你在项目中如何使用它?
解答:
- 定义: 反射是指在运行状态中,对于任意一个类,都能够获取其所有属性和方法;对于任意一个对象,都能调用其任意方法和属性。
- 项目应用:
- 动态代理: 在 Spring Security 中,利用反射实现 AOP,动态增强权限校验逻辑。
- 框架配置: 在项目初始化时,通过反射读取注解信息,动态注册 Bean 或生成 SQL 语句。
6. 简述 String、StringBuilder 和 StringBuffer 的区别。 [citation:4][citation:5]
解答:
- String: 不可变类,每次修改都会创建新对象,线程安全(天然安全),适合少量字符串操作或常量 [citation:4]。
- StringBuilder: 可变类,线程不安全,但单线程下性能最高,适合单线程大量字符串拼接 [citation:5]。
- StringBuffer: 可变类,线程安全(方法加 synchronized),适合多线程环境下的字符串操作 [citation:4][citation:5]。
7. 自动装箱和拆箱是什么? [citation:4]
解答:
- 自动装箱: 基本数据类型自动转换为对应的包装类对象,如
Integer i = 10;。 - 自动拆箱: 包装类对象自动转换为对应的基本数据类型,如
int j = new Integer(20);。 - 注意: 频繁的装箱拆箱可能影响性能,例如在循环中进行数值计算时。
面向对象篇
8. Java 的面向对象三大特性是什么? [citation:1][citation:4][citation:5]
解答:
- 封装: 隐藏对象的内部实现细节,通过公共方法暴露接口,提高安全性和可维护性。例如类的
private属性通过getter/setter访问 [citation:1][citation:5]。 - 继承: 子类继承父类的属性和方法,实现代码复用。Java 是单继承,一个类只能有一个父类 [citation:4][citation:5]。
- 多态: 同一行为在不同对象上表现不同。需满足三个条件:继承、方法重写、父类引用指向子类对象 [citation:1][citation:5]。
9. 方法重载(Overload)和方法重写(Override)有什么区别? [citation:1][citation:4]
解答:
- 重载: 发生在同一个类中,方法名相同但参数列表不同(类型、个数、顺序),与返回值无关,属于编译时多态 [citation:1][citation:4]。
- 重写: 发生在父子类中,子类对父类允许访问的方法重新实现,方法签名必须相同,属于运行时多态 [citation:1][citation:4]。
10. 抽象类和接口有什么区别? [citation:5]
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 继承方式 | 单继承(extends) | 多实现(implements) |
| 方法 | 可有抽象方法和具体方法 | 默认 public abstract(Java 8 后有 default/static 方法) |
| 属性 | 可定义各种修饰符的变量 | 只能是 public static final 常量 |
| 构造器 | 有 | 无 |
| 设计目的 | 代码复用(是什么 + 怎么做) | 定义行为规范(是什么) |
11. final 关键字有哪些用法? [citation:5]
解答:
- 修饰类: 类不可被继承(如 String 类)。
- 修饰方法: 方法不可被重写。
- 修饰变量: 变量变为常量。
- 基本类型:值不可变。
- 引用类型:引用地址不可变,但对象内容可修改 [citation:5]。
12. static 关键字的作用是什么? [citation:5]
解答:
- 静态变量: 类的所有实例共享,通过类名直接访问,内存中仅一份。
- 静态方法: 属于类,可直接通过类名调用,不能直接访问非静态成员。
- 静态代码块: 类加载时执行一次,用于初始化静态资源。
- 静态内部类: 不依赖外部类实例,可直接创建。
集合框架篇
13. 请对比 HashMap 和 Hashtable 的区别。 [citation:2][citation:6]
解答:
- 线程安全:
Hashtable是线程安全的(方法用synchronized修饰),HashMap非线程安全 [citation:6]。 - 性能:
HashMap性能更高,单线程环境下优先使用。 - Null 值:
HashMap允许一个 null 键和多个 null 值;Hashtable不允许 null 键或 null 值 [citation:6]。 - 扩容机制:
HashMap初始容量 16,扩容为 2 倍;Hashtable初始容量 11,扩容为 2 倍+1。
14. ConcurrentHashMap 是如何实现线程安全的? [citation:1][citation:2][citation:6]
解答:
- JDK 1.7: 采用分段锁(Segment),将数据分成多个段,每个段独立加锁,提高并发度 [citation:6]。
- JDK 1.8+: 改用
synchronized+ CAS(乐观锁)锁住链表或红黑树的头节点,粒度更细,并发性能更好 [citation:1][citation:2][citation:6]。 - 核心特性: 读操作基本无锁,写操作只锁住当前操作的桶,支持高并发场景。
15. ArrayList 和 LinkedList 分别适用于什么场景? [citation:1][citation:2][citation:6]
解答:
ArrayList: 基于动态数组实现。适合随机访问(get和set)频繁的场景,时间复杂度 O(1);但在中间插入/删除元素效率低,因为需要移动元素 [citation:1][citation:6]。LinkedList: 基于双向链表实现。适合频繁插入/删除(尤其是头部和尾部)的场景,时间复杂度 O(1);但随机访问效率低,需要遍历 [citation:1][citation:6]。
16. 如何保证遍历集合时不被其他线程修改? [citation:2][citation:6]
解答:
- Fail-Fast 机制: 使用
java.util包下的集合(如ArrayList、HashMap),当多个线程并发修改时,会抛出ConcurrentModificationException[citation:2][citation:6]。 - Fail-Safe 机制: 使用
java.util.concurrent包下的集合(如CopyOnWriteArrayList、ConcurrentHashMap),它们通过复制原数据或分段锁来保证遍历时不被修改,不会抛出异常 [citation:6]。
17. HashSet 如何保证元素不重复? [citation:2]
解答:
- 依赖元素的
hashCode()和equals()方法:- 先计算元素的哈希值,找到对应的存储位置。
- 如果该位置无元素,直接存入。
- 如果有元素,则通过
equals()比较是否相等。若equals()返回 false,则以链表/红黑树形式存储;若返回 true,则拒绝存入 [citation:2]。
- 核心原则: 重写
equals()必须重写hashCode(),否则可能导致相同元素被重复存入 [citation:2]。
18. HashMap 的底层数据结构是怎样的? [citation:1][citation:2]
解答:
- JDK 1.7: 数组 + 链表。
- JDK 1.8+: 数组 + 链表 + 红黑树。当链表长度 > 8 且数组容量 > 64 时,链表转为红黑树,优化查询效率 [citation:1][citation:2]。
19. 什么是迭代器的 fail-fast 机制? [citation:6]
解答:
- 当使用迭代器遍历集合时,如果集合结构被修改(如增加、删除元素),迭代器会立即抛出
ConcurrentModificationException,停止遍历 [citation:6]。 - 这是通过
modCount计数器实现的:迭代器初始化时记录modCount,每次调用next()都会检查modCount是否被修改。 - 并发集合类(如
ConcurrentHashMap)采用 fail-safe 机制,不会抛出此异常 [citation:6]。
并发编程篇
20. synchronized 和 ReentrantLock 有什么区别? [citation:1][citation:7]
解答:
- 使用方式:
synchronized是关键字,自动加解锁;ReentrantLock是 API 类,需要手动lock()和unlock(),通常放在finally块中释放 [citation:1]。 - 功能特性:
ReentrantLock提供了更多高级功能,如公平锁、可中断锁、超时获取锁、多个 Condition [citation:1]。 - 性能: 早期
synchronized性能较差,但现在 JVM 对两者做了优化,性能差异不大。建议简单场景用synchronized,复杂并发场景用ReentrantLock[citation:1]。
21. 什么是 volatile 关键字?它能保证原子性吗? [citation:1][citation:7]
解答:
- 作用:
- 可见性: 保证一个线程修改变量后,其他线程能立即看到最新值(禁止线程本地缓存)[citation:1]。
- 有序性: 禁止指令重排序优化(如 DCL 单例模式中必须用
volatile)[citation:1]。
- 原子性:
volatile不能保证原子性。例如count++操作,即使变量被volatile修饰,多线程下仍会数据错乱,需用synchronized或AtomicInteger保证原子性 [citation:1][citation:7]。
22. 说说线程池的核心参数(ThreadPoolExecutor)。 [citation:1]
解答:
- corePoolSize: 核心线程数,即使空闲也会保留。
- maximumPoolSize: 最大线程数。
- keepAliveTime: 非核心线程空闲存活时间。
- workQueue: 任务等待队列(如
LinkedBlockingQueue、SynchronousQueue)。 - threadFactory: 创建线程的工厂(可自定义线程名)。
- handler: 拒绝策略(如
AbortPolicy抛出异常、CallerRunsPolicy让提交线程自己执行)。
23. Java 中实现多线程有哪几种方式? [citation:3][citation:4]
解答:
- 继承 Thread 类: 重写
run()方法,创建子类对象调用start()。简单但不能继承其他类 [citation:3]。 - 实现 Runnable 接口: 实现
run()方法,作为参数传入 Thread。可继承其他类,无返回值 [citation:3][citation:4]。 - 实现 Callable 接口: 实现
call()方法,有返回值且可抛出异常,需配合 FutureTask [citation:3][citation:4]。 - 线程池: 通过
Executors创建线程池,提交 Runnable 或 Callable 任务 [citation:3]。
24. 什么是线程安全问题?如何解决? [citation:4]
解答:
- 定义: 当多个线程同时访问共享资源时,导致数据不一致、脏读等问题 [citation:4]。
- 解决方法:
- 同步代码块/方法: 使用
synchronized关键字 [citation:4]。 - Lock 接口: 使用
ReentrantLock等显式锁 [citation:4]。 - 原子类: 使用
AtomicInteger等 CAS 实现的无锁并发 [citation:1][citation:4]。 - 线程安全集合: 使用
ConcurrentHashMap、CopyOnWriteArrayList等 [citation:2]。
- 同步代码块/方法: 使用
25. sleep() 和 wait() 有什么区别?
解答:
- 所属类:
sleep()是 Thread 类的静态方法;wait()是 Object 类的方法。 - 锁释放:
sleep()不释放锁;wait()会释放锁,并进入等待队列。 - 使用场景:
sleep()用于暂停执行一段时间;wait()通常用于线程间通信,需要配合notify()/notifyAll()唤醒。 - 唤醒方式:
sleep()时间到自动唤醒;wait()需要被唤醒或超时唤醒。
JVM 与性能优化篇
26. 请描述 JVM 的内存区域划分。 [citation:1][citation:8]
解答:
- 线程私有:
- 程序计数器: 当前线程执行的字节码行号指示器 [citation:8]。
- 虚拟机栈: 存储局部变量表、操作数栈、方法出口等,每个方法调用对应一个栈帧入栈出栈 [citation:1][citation:8]。
- 本地方法栈: 为 native 方法服务 [citation:8]。
- 线程共享:
- 堆: 存放对象实例,垃圾回收的主要区域(分新生代、老年代)[citation:1][citation:8]。
- 方法区: 存储类信息、常量、静态变量、即时编译后的代码(元空间/Metaspace)[citation:1][citation:8]。
27. 你遇到过哪些 OOM(OutOfMemoryError)场景?如何排查? [citation:1]
解答:
- 常见场景:
- 堆溢出: 对象过多且无法回收(死循环创建对象、内存泄漏)。排查用
jmap或 MAT 分析堆转储文件 [citation:1]。 - 栈溢出: 递归调用过深或线程数过多。检查递归逻辑或线程池配置。
- 元空间溢出: 加载类过多(热部署、反射生成类)。检查是否有类加载器泄漏。
- 堆溢出: 对象过多且无法回收(死循环创建对象、内存泄漏)。排查用
- 排查步骤:
- 添加 JVM 参数
-XX:+HeapDumpOnOutOfMemoryError自动生成堆转储。 - 用 MAT 或 JProfiler 分析大对象引用链 [citation:1]。
- 结合业务代码,定位内存泄漏点(如未关闭的资源、不正确的缓存)。
- 添加 JVM 参数
28. JVM 常见的垃圾回收算法有哪些? [citation:1][citation:8]
解答:
- 标记-清除: 标记存活对象,清除未标记对象。会产生内存碎片 [citation:1][citation:8]。
- 复制算法: 将内存分为两块,每次只使用一块,存活对象复制到另一块。适用于新生代(无碎片,但内存利用率低)[citation:1][citation:8]。
- 标记-整理: 标记存活对象后,将所有存活对象向一端移动,清理边界外内存。适用于老年代(无碎片,效率稍低)[citation:1][citation:8]。
- 分代收集: 根据对象存活周期,新生代用复制算法,老年代用标记-整理或标记-清除 [citation:8]。
29. 什么是双亲委派模型? [citation:1]
解答:
- 定义: 类加载器收到类加载请求时,先将请求委派给父类加载器加载,只有当父加载器无法加载时,子加载器才尝试自己加载 [citation:1]。
- 作用:
- 避免类被重复加载。
- 保证核心类库(如
java.lang.String)的安全,防止被篡改 [citation:1]。
新特性与编码实践
30. Java 8 的 Optional 类有什么作用?如何使用?
解答:
- 作用: 主要解决
NullPointerException问题,提供一种更优雅的方式来处理可能为 null 的值。 - 常用方法:
ofNullable():创建可能为空的 Optional。orElse()/orElseGet():值为空时返回默认值。map()/flatMap():对值进行转换。ifPresent():值存在时执行操作。
- 示例:
Optional.ofNullable(user).map(User::getAddress).orElse("默认地址"),避免了多层 null 检查。
31. 什么是方法引用(Method Reference)?有哪些类型?
解答:
- 定义: 方法引用是 Lambda 表达式的简化写法,用
::操作符表示。 - 类型:
- 静态方法引用:
类名::静态方法名(如Math::max) - 实例方法引用:
对象::实例方法名 - 特定类型任意对象方法引用:
类名::实例方法名(如String::length) - 构造方法引用:
类名::new
- 静态方法引用:
32. 如何写出更优雅的 Java 代码?(编码规范与最佳实践)
解答:
- 遵循命名规范: 类名大驼峰,方法/变量小驼峰,常量全大写。
- 合理使用 Optional: 避免返回 null,减少 NPE 风险。
- 善用 Stream API: 替代复杂的循环和条件判断。
- 接口设计原则: 面向接口编程而非实现类 [citation:1]。
- 异常处理: 不要捕获了异常什么都不做,至少打印日志。
- 单测覆盖: 关键业务逻辑必须有单元测试(JUnit + Mockito)。
- 代码复用: 提取公用方法,避免重复代码(DRY 原则)。
33. 什么是 Java 的 SPI 机制?
解答:
- 定义: SPI(Service Provider Interface)是 JDK 内置的服务提供发现机制,用于实现框架的扩展性。
- 工作原理: 在
META-INF/services目录下创建以接口全限定名命名的文件,内容为实现类的全限定名。通过ServiceLoader加载实现类。 - 项目应用: 在开发 SDK 或框架时,利用 SPI 允许第三方扩展功能(如 JDBC 驱动加载、Spring 的
spring.factories)。
34. 谈谈你对“面向接口编程”的理解,并结合项目说明。 [citation:1]
解答:
- 理解: 面向接口编程意味着调用者只依赖接口定义,而不依赖具体实现类。这降低了代码耦合度,提高了可扩展性和可测试性 [citation:1]。
- 项目应用: 在“科研人员画像系统”中,我定义了
ScoreCalculator接口,然后分别实现PaperScoreCalculator(论文得分)、ProjectScoreCalculator(项目得分)等。当新增得分规则时,只需新增实现类,无需修改调用方的业务逻辑,符合开闭原则。
部分信息可能已经过时









