面试题 1:final 关键字的底层含义是什么?能保证线程安全吗?
很多人只会背:
- final 修饰变量 → 值不能变
- final 修饰方法 → 不能重写
- final 修饰类 → 不能继承
但这完全不够。
✅ 深度解析
① final 局部变量 → 只保证“引用不变”,不保证对象不变
final List<String> list = new ArrayList<>();
list = new LinkedList<>(); // ❌ 编译报错
list.add("A"); // ✔ 正常,内容可变
final 修饰的是“引用地址”,不是“对象状态”。
② final 修饰字段可让 JVM 做优化(逃逸分析)
JVM 可能进行:
- 常量折叠(Constant Folding)
- 去除重复读取
- 方法内联优化
使性能更好。
③ final 并不能保证线程安全(核心考点)
例如:
final User user = new User();
user.setName("Tom"); // 内容仍可变,线程仍不安全
final ≠ immutable(不可变对象)
真正的不可变类必须:
- 所有字段 private + final
- 无 setter
- 对象内部状态不可修改
- 若有引用字段,必须深拷贝
⭐ 面试官追问
- final 和 volatile 的区别是什么?
- final 能否阻止指令重排?
- 构造函数里写 this 会破坏 final 安全性吗?
❌ 易错点
- 以为 final = 线程安全(错误)
- 以为 final 对象内容不能变
- 忽略 final 的 JVM 优化价值
面试题 2:StringBuilder 为什么不是线程安全的?底层如何实现?
✅ 深度解析
① StringBuilder 内部使用可变数组
JDK 8 源码:
char[] value;
int count;
追加时会改变数组内容,因此:
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
内部是直接修改同一个 char 数组。
② 这种可变结构导致线程不安全
如果多线程同时 append:
sb.append("X");
sb.append("Y");
两个线程可能同时修改 value 数组的同一位置 → 数据乱序。
③ StringBuffer 为什么线程安全?
因为方法全部加了 synchronized:
public synchronized StringBuffer append(String str) {
...
}
⭐ 面试官追问
- StringBuilder 在什么场景下会比 StringBuffer 快?
- StringBuilder 扩容机制是什么?
- 线程安全又希望性能高,应该用什么?(答案:StringBuilder + 局部变量 或 StringBuffer + 不共享)
❌ 易错点
- 以为 StringBuilder 内部使用 synchronized(错)
- 误把“线程不安全”理解为“不能用”
- 不理解为什么很多框架内部用 StringBuilder(因为局部变量天然线程安全)
面试题 3:字符串拼接 “+” 和 StringBuilder 的效率差异,底层发生了什么?
✅ 深度解析
① 常见误解:认为 “+” 一定很慢
其实要区分两种情况!
🔸 情况 1:常量拼接(编译期优化)
String s = "a" + "b";
编译器会优化成:
String s = "ab";
→ 不会创建 StringBuilder
→ 不会运行期拼接
→ 效率极高!
🔸 情况 2:变量拼接(运行期)
String a = "x";
String b = "y";
String s = a + b;
编译器会自动生成类似:
StringBuilder sb = new StringBuilder();
sb.append(a);
sb.append(b);
s = sb.toString();
也就是说:
✔ 变量拼接一定会生成 StringBuilder 对象
✔ 连续 + 操作会创建多个中间对象,效率低
⭐ 面试官追问
- 为什么循环拼接字符串要手动用 StringBuilder?
- JDK 9 的新版 String(byte[] + coder)对拼接有什么影响?
- “+” 拼接一定慢吗?(考点:常量折叠)
❌ 易错点
- 不区分常量拼接 vs 变量拼接
- 误认为 + 拼接永远慢
- 不知道编译器会自动转成 StringBuilder
面试题 4:类加载的双亲委派机制是什么?为什么要这么设计?
✅ 深度解析
Java 类加载器结构:
BootstrapClassLoader
↓
ExtClassLoader
↓
AppClassLoader
↓
CustomClassLoader
⭐ 双亲委派机制的核心流程:
当一个类加载器想加载类时:
- 先让父加载器尝试加载
- 若父加载器找不到
- 自己再尝试加载
⭐ 为什么要这样设计?
① 防止核心类被篡改(最重要)
例如用户写了一个:
java.lang.String
如果没有双亲委派,它可能覆盖 JDK 的 String 类,程序会直接崩溃。
② 提高缓存命中率
JDK 核心类只加载一次,由父加载器缓存。
③ 确保类一致性
不同 ClassLoader 加载的类即使字节码一样,也不是一个类。
双亲委派避免冲突。
⭐ 面试官追问
- 如何破坏双亲委派机制?
- SPI 为什么要破坏双亲委派?
- 自定义类加载器有哪些场景?
❌ 易错点
- 以为双亲委派只能向上委托一次
- 以为每层 ClassLoader 都能加载所有类
面试题 5:运行时常量池(Runtime Constant Pool)和静态常量池有什么区别?
⭐ 正确理解:
| 常量池类型 | 存储位置 | 存的是什么 |
|---|---|---|
| 静态常量池 | class 文件 | 编译期生成的符号引用、字面量 |
| 运行时常量池 | 方法区 / 元空间 | 将符号引用变成运行时能用的直接引用 |
🔸 静态常量池在编译期就确定
例如:
String s = "abc";
“abc” 在 class 文件的常量池中。
🔸 运行时常量池负责“解析引用”
如:
- 方法引用
- 字段引用
- 类引用
在类加载、解析阶段完成。
⭐ 为什么要区分?
因为 JVM 执行指令时需要“真正的内存地址”,不是 class 文件里的符号。
比如:
#1 = Class java/lang/String
在运行时常量池中会解析成真实 Class 对象引用。
⭐ 面试官追问
- String.intern() 和运行时常量池的关系?
- JDK 7 和 8 常量池位置有什么变化?
- 为什么 PermGen 被移除?
❌ 易错点
- 把“字符串常量池”和“运行时常量池”等同
- 不了解 class 常量池是静态存储
🟩 结语(可直接复制)
本篇我们深入分析了:
- final 的底层语义与线程安全
- StringBuilder 与字符串拼接的原理
- 类加载机制中的双亲委派
- 常量池的静态与运行时差异
这些内容是 Java 面试中的高频考点,也是构建底层理解的必备基础。
下一篇将继续解析:
《第 3 篇:ArrayList / LinkedList / fail-fast》
欢迎收藏本专栏,也欢迎留言你想看的题目!
395

被折叠的 条评论
为什么被折叠?



