在 Java 中,String 被设计为不可变类(immutable),即一旦创建,其值就不能被修改。这是 Java 语言设计中的一个经典决策,背后涉及性能、安全、并发等多方面考虑。
一、什么是不可变?
不可变意味着对象的状态在创建后无法改变。对于 String:
- 没有提供任何可以修改内部字符数组的方法(如
setCharAt)。 - 所有看似修改字符串的方法(如
concat、replace、substring)都会返回一个新的String对象,原对象不变。
二、实现不可变的关键
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; // JDK 8 及以前;JDK 9+ 为 byte[]
/** Cache the hash code for the string */
private int hash; // Default to 0
// ... 其他成员和方法
}
final类:不能被继承,防止子类破坏不可变性。private final char[] value:字符数组被final修饰且私有,一旦初始化就不能指向其他数组,且不对外暴露修改数组内容的方法。- 不提供任何 setter 方法:所有看似修改的操作都返回新对象。
三、为什么设计为不可变?
1. 字符串常量池的需要
Java 为了节省内存,设计了字符串常量池(String Pool)。当创建一个字符串字面量时,JVM 会先检查池中是否存在相同内容的字符串,如果存在则返回引用,否则创建新对象放入池中。
如果字符串可变,那么一个引用修改了字符串内容,会导致池中其他引用指向的值也被改变,产生不可预知的错误。不可变性保证了常量池的安全复用。
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true,同一个对象
2. 安全性
字符串广泛用于类加载、网络连接、文件路径、数据库 URL、密码等敏感场景。如果字符串可变,攻击者可能通过反射等手段修改字符串内容,造成安全隐患。
例如,以下代码若 filename 可变,则可能在安全检查通过后被篡改:
String filename = "/safe/path/config";
if (checkSecurity(filename)) {
openFile(filename); // 若 filename 被修改,可能绕过安全校验
}
3. 线程安全
不可变对象天然是线程安全的,无需同步即可在多个线程间共享。String 的不可变性使其可以安全地用于多线程环境,提高并发性能。
4. 哈希缓存(性能优化)
字符串经常用作 HashMap、HashSet 的键。由于不可变,其哈希码可以在创建时计算并缓存(hash 字段),之后直接返回,无需重复计算。这大大提高了哈希容器的性能。
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
// 计算哈希并赋给 hash
}
return h;
}
5. 类加载机制
JVM 使用字符串来表示类名、包名等。如果字符串可变,恶意代码可能修改正在加载的类名,破坏类加载器的安全机制。
四、常见误区与细节
Q: 为什么 String 被声明为 final?
- 防止子类继承并重写方法,从而破坏不可变性。例如,若子类提供了修改内部
value的方法,则不可变性被打破。
Q: 反射能否修改 String 内容?
-
理论上可以,通过反射获取
value字段并修改其数组内容。但这会破坏封装性,不推荐使用,且可能导致难以排查的 bug。// 示例:通过反射修改 String 内容(仅供理解,切勿在正式代码中使用) String str = "hello"; Field field = String.class.getDeclaredField("value"); field.setAccessible(true); char[] value = (char[]) field.get(str); value[0] = 'H'; System.out.println(str); // 输出 "Hello"
Q: JDK 9 之后 String 内部存储的变化?
- JDK 9 引入了 Compact Strings,将内部存储从
char[]改为byte[],并增加一个coder字段标识编码(Latin-1 或 UTF-16)。但依然保持不可变性。
五、总结
原因
说明
字符串常量池
实现字符串复用,避免内存浪费
安全性
防止敏感字符串被篡改
线程安全
无需同步即可安全共享
哈希缓存
提高作为键时的性能
类加载机制
保证类加载过程的安全
正是因为这些原因,Java 将 String 设计为不可变类,并成为面试中频繁考察的知识点。理解其背后原理,有助于写出更健壮的代码。