1. 引言

在 Java 中,String 是不可变的(immutable)。这个问题在面试中非常常见:“为什么 Java 要设计成 String 不可变?”

Java 之父 James Gosling 曾在一个采访中被问到什么时候该使用不可变对象,他的回答是:

我会在任何可能的情况下使用不可变对象。

他还进一步解释了不可变性带来的优势:比如缓存、安全性、无需复制即可复用、线程安全等。

本文将深入探讨 Java 语言设计者为何将 String 类设计为不可变类。

2. 什么是不可变对象?

不可变对象:指一个对象在创建完成后,其内部状态永远无法被修改
这意味着:

  • 你不能改变该对象的引用指向(除非重新赋值)
  • 更不能通过任何方式修改其内部字段的值

举个例子,一旦一个 String 对象被创建,你就不能再“修改”它的内容。所谓的 s += "abc" 其实是创建了一个新对象。

我们有专门的文章讲解 Java 中的不可变对象,感兴趣可以查阅《Java 不可变对象详解》。

3. 为什么 String 要设计成不可变?

String 设计为不可变,核心原因有四个:✅ 字符串常量池优化、✅ 安全性、✅ 线程安全、✅ 性能提升(尤其是哈希缓存)

下面我们逐个展开。

3.1 字符串常量池(String Pool)

String 是 Java 中使用最频繁的数据类型之一。为了节省内存,JVM 引入了 字符串常量池(String Pool) —— 一个专门存储字符串字面量的特殊内存区域。

由于 String 是不可变的,JVM 才能放心地对相同内容的字符串进行复用。这个过程叫做 interning(驻留)

看下面这个例子:

String s1 = "Hello World";
String s2 = "Hello World";
         
assertThat(s1 == s2).isTrue();

⚠️ 注意:这里用的是 ==,而不是 equals()
结果为 true,说明 s1s2 指向的是同一个对象,它们都从字符串池中获取了同一个实例。

这就是不可变性的功劳:因为内容不会变,所以可以安全共享。

Why String Is Immutable In Java

如果没有不可变性,这种共享机制就会出问题 —— 一个线程修改了字符串内容,其他所有引用它的变量都会受影响,这显然是灾难性的。

我们有专门文章讲解字符串池,详见《Java 字符串池详解》。

3.2 安全性

String 被广泛用于存储敏感信息,比如:

  • 用户名、密码
  • 数据库连接 URL
  • 网络地址
  • JVM 类加载器中的类名

如果 String 是可变的,那安全体系就会崩塌。来看一个典型场景:

void criticalMethod(String userName) {
    // 执行安全校验
    if (!isAlphaNumeric(userName)) {
        throw new SecurityException(); 
    }
    
    // 做一些其他操作
    initializeDatabase();
    
    // 关键操作:执行 SQL 更新
    connection.executeUpdate("UPDATE Customers SET Status = 'Active' " +
      " WHERE UserName = '" + userName + "'");
}

我们对 userName 做了合法性校验,但注意:调用方仍然持有这个 String 的引用

❌ 如果 String 是可变的,调用方完全可以在我们校验之后、执行 SQL 之前,偷偷把 userName"alice" 改成 "alice'; DROP TABLE Customers; --"

虽然 Java 的 String 实际上无法被修改,但假设它是可变的,这就构成了典型的 SQL 注入漏洞。

更糟的是,如果多个线程共享这个 String,另一个线程也可能在检查之后修改其内容,导致逻辑错乱。

✅ 正因为 String 不可变,我们才能确保:一旦通过校验,它的值就永远安全

3.3 线程安全(Synchronization)

不可变性天然带来线程安全。

✅ 所有不可变对象都是线程安全的,String 也不例外。

多线程环境下,多个线程可以同时读取同一个 String 对象,完全不需要加锁。
因为没人能改它,所以不存在竞态条件(race condition)。

如果某个线程“想修改”一个 String,比如:

String s = "hello";
s = s + " world";  // 实际上是创建新对象

这并不会修改原对象,而是在字符串池中生成一个新的 String,然后把引用指向它。原对象依然安全地被其他线程使用。

⚠️ 想想看,如果 String 是可变的,每次拼接都要同步,那性能得多差?StringBuffer 都得加锁,更何况是高频使用的 String

3.4 哈希值缓存(Hashcode Caching)

String 被大量用于 HashMapHashSetHashTable 等基于哈希的集合中。

这些集合依赖 hashCode() 进行桶(bucket)定位。如果每次都要重新计算哈希值,性能会大打折扣。

✅ 由于 String 不可变,它的哈希值也永远不会变。因此,String 类内部做了优化:

  • 第一次调用 hashCode() 时计算并缓存哈希值
  • 后续调用直接返回缓存值

源码节选:

public final class String {
    private int hash; // 默认为 0

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
}

✅ 这种缓存机制极大提升了 HashMap<String, V> 的性能。

❌ 反观可变字符串:如果插入时 hashCode=100,后来内容被修改,再 gethashCode=200,那就找不到原来的值了 —— 直接导致数据“丢失”。

3.5 性能优化

综合来看,不可变性带来的性能提升是全方位的:

  • 内存节省:字符串池避免重复对象
  • 访问加速:哈希缓存减少重复计算
  • 减少同步开销:天然线程安全,无需锁
  • GC 友好:短生命周期字符串快速回收,常量池长期复用

由于 String 是 Java 程序中最常用的数据结构之一,它的性能优化对整个应用都有显著影响。

4. 总结

String 之所以设计为不可变,根本原因在于:

我们希望 String 引用能像基本类型一样安全传递 —— 无论是在方法间传递,还是跨线程共享,都不用担心它指向的内容会被悄悄修改。

正是这种设计,支撑起了:

  • 字符串常量池的内存优化
  • 敏感信息的安全保障
  • 多线程环境下的无锁并发
  • 哈希集合中的高效缓存

⚠️ 虽然不可变性带来了一些“看似不方便”的地方(比如频繁拼接要用 StringBuilder),但权衡之下,这是 Java 设计中最成功的决策之一。

下次面试官再问“为什么 String 不可变”,别只答“因为安全”,要把这四点(池、安全、同步、哈希)都拎出来,简单粗暴,稳稳拿捏。