Java 面试核心:String 不可变性与常量池深度解析
一、String 不可变性的定义
String 的不可变性(Immutability)是指:一旦创建,String 对象的内容不能被修改。任何看似"修改"字符串的操作,实际上都是创建了一个新的 String 对象。
java
String str = "Hello";
str = str + " World"; // 创建了新对象,原"Hello"对象未被修改
二、为什么 String 要设计为不可变?
1. 安全性(Security)
- 字符串广泛用于网络连接、文件路径、数据库连接等敏感场景
- 不可变性防止字符串内容被恶意篡改,避免安全漏洞
java
// 假设 String 可变,攻击者可能修改连接字符串
String url = "https://secure.bank.com";
// 如果被修改,可能导致连接到恶意站点
2. 线程安全(Thread Safety)
- 不可变对象天然线程安全,无需同步机制
- 多个线程共享 String 对象时不会出现数据不一致
3. 字符串常量池(String Pool)的实现基础
- 只有不可变,才能放心地让多个引用指向同一对象
- 大幅节省内存空间,提高性能
4. HashCode 缓存
- String 的
hashCode()在第一次调用后会被缓存 - 作为 HashMap 的 Key 时性能极高,避免重复计算
java
// String 源码中的 hash 字段缓存
public final class String {
private int hash; // 缓存 hashCode,默认 0
// ...
}
三、String 不可变性的源码保证
java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** 字符数组被 final 修饰,引用不可变 */
private final char value[]; // JDK 8 及之前
// JDK 9+ 改为 byte[] value,支持 Latin-1 压缩
/** String 类本身被 final 修饰,禁止继承和篡改 */
/** 所有修改方法都返回新对象,原对象保持不变 */
public String replace(char oldChar, char newChar) {
// ... 创建新字符数组,返回新 String 对象
return new String(buf, true);
}
}
三大保证机制:
| 机制 | 作用 |
|---|---|
final class |
禁止继承,防止子类破坏不可变性 |
final char[] value |
引用不可变(JDK 9+ 为 byte[]) |
| 无修改方法 | 所有操作返回新对象,原对象内容不变 |
⚠️ 注意 :
final只保证引用不可变,如果 value[] 暴露出去,内容仍可被修改。String 通过封装确保数组不暴露。
四、字符串常量池(String Pool / String Intern Pool)
1. 什么是常量池?
字符串常量池是 JVM 堆内存中的一块特殊区域(JDK 7+ 从永久代移到了堆),用于存储字符串字面量,避免重复创建相同字符串。
2. 字符串创建方式对比
java
// 方式1:字面量创建(推荐)------ 使用常量池
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2); // true,指向常量池同一对象
// 方式2:new 创建 ------ 堆中新建对象
String s3 = new String("Java");
System.out.println(s1 == s3); // false,s3 是堆中新对象
// 方式3:intern() 手动入池
String s4 = new String("Java").intern();
System.out.println(s1 == s4); // true,强制入池后指向同一对象
五、面试高频考点与代码分析
考点1:创建了几个对象?
java
String s = new String("abc");
答案:2 个对象(如果常量池已有 "abc" 则 1 个)
- 常量池中的字符串字面量
"abc"(类加载时创建或已存在) new在堆中创建的 String 对象
java
String s1 = "a" + "b"; // 编译期优化为 "ab",1 个对象
String s2 = "a" + new String("b"); // 运行时创建,3+ 个对象
考点2:intern() 方法详解
java
String s1 = new String("hello"); // 堆中对象
String s2 = s1.intern(); // 将 s1 放入常量池(或返回已有引用)
String s3 = "hello"; // 指向常量池
System.out.println(s1 == s2); // JDK 6: false, JDK 7+: true(视情况而定)
System.out.println(s2 == s3); // true
JDK 6 vs JDK 7+ 的区别:
- JDK 6 :
intern()将字符串复制到永久代常量池,返回常量池引用 - JDK 7+ :
intern()将堆中字符串引用放入常量池,节省内存
考点3:String 拼接的陷阱
java
// 编译期常量折叠
String s1 = "a" + "b" + "c"; // 编译为 "abc",常量池 1 个对象
// 运行期 StringBuilder 拼接
String s2 = "a";
for (int i = 0; i < 3; i++) {
s2 += "b"; // 每次循环都创建 StringBuilder → toString() → 新对象
}
// 等价于:
// s2 = new StringBuilder(s2).append("b").toString();
优化建议 :循环内大量拼接使用 StringBuilder 手动控制
java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("data");
}
String result = sb.toString();
六、String、StringBuilder、StringBuffer 对比
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变) | 不安全 | 安全(synchronized) |
| 性能 | 低(频繁创建对象) | 高 | 中(同步开销) |
| 使用场景 | 字符串常量、少量操作 | 单线程大量拼接 | 多线程大量拼接 |
七、面试问答模板
Q:String 为什么是不可变的?
String 通过
final类、final字符数组引用和封装设计实现不可变。这样设计的原因包括:1)安全性,防止敏感字符串被篡改;2)线程安全,天然支持多线程共享;3)支持字符串常量池,节省内存;4)HashCode 可缓存,提高哈希集合性能。
Q:new String("abc") 创建了几个对象?
创建 1 或 2 个对象。如果常量池已有
"abc",则只在堆中创建 1 个新对象;如果常量池没有,则先在常量池创建"abc",再在堆中创建对象,共 2 个。
Q:String s1 = "a"; String s2 = "a"; s1 == s2?
true。字面量创建会先在常量池查找,存在则直接返回引用,所以 s1 和 s2 指向常量池同一对象。
Q:如何打破 String 的不可变性?(进阶)
理论上可通过反射修改
value数组,但强烈不推荐,会破坏常量池机制、HashCode 缓存,导致严重 Bug。
java
// 仅供了解,生产环境严禁使用
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[0] = 'X'; // 修改了字符串内容!
八、总结
String 的不可变性是 Java 语言设计的精妙之处,它与常量池机制共同构成了高效的字符串处理体系。理解这些底层原理,不仅能应对面试,更能写出高性能、线程安全的代码。
核心要点速记:
- 不可变 = 安全 + 线程安全 + 可缓存 + 可共享
- 字面量用常量池,
new创建在堆中 - 大量拼接用
StringBuilder,多线程用StringBuffer intern()可手动入池,但需谨慎使用(JDK 7+ 不会复制对象)