String 为什么不可变?面试必问

0 阅读1分钟

在 Java 中,String 被设计为不可变类(immutable),即一旦创建,其值就不能被修改。这是 Java 语言设计中的一个经典决策,背后涉及性能、安全、并发等多方面考虑。

一、什么是不可变?

不可变意味着对象的状态在创建后无法改变。对于 String

  • 没有提供任何可以修改内部字符数组的方法(如 setCharAt)。
  • 所有看似修改字符串的方法(如 concatreplacesubstring)都会返回一个新的 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. 哈希缓存(性能优化)

字符串经常用作 HashMapHashSet 的键。由于不可变,其哈希码可以在创建时计算并缓存(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 设计为不可变类,并成为面试中频繁考察的知识点。理解其背后原理,有助于写出更健壮的代码。