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
,说明 s1
和 s2
指向的是同一个对象,它们都从字符串池中获取了同一个实例。
这就是不可变性的功劳:因为内容不会变,所以可以安全共享。
如果没有不可变性,这种共享机制就会出问题 —— 一个线程修改了字符串内容,其他所有引用它的变量都会受影响,这显然是灾难性的。
我们有专门文章讲解字符串池,详见《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
被大量用于 HashMap
、HashSet
、HashTable
等基于哈希的集合中。
这些集合依赖 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
,后来内容被修改,再 get
时 hashCode=200
,那就找不到原来的值了 —— 直接导致数据“丢失”。
3.5 性能优化
综合来看,不可变性带来的性能提升是全方位的:
- ✅ 内存节省:字符串池避免重复对象
- ✅ 访问加速:哈希缓存减少重复计算
- ✅ 减少同步开销:天然线程安全,无需锁
- ✅ GC 友好:短生命周期字符串快速回收,常量池长期复用
由于 String
是 Java 程序中最常用的数据结构之一,它的性能优化对整个应用都有显著影响。
4. 总结
String
之所以设计为不可变,根本原因在于:
✅ 我们希望
String
引用能像基本类型一样安全传递 —— 无论是在方法间传递,还是跨线程共享,都不用担心它指向的内容会被悄悄修改。
正是这种设计,支撑起了:
- 字符串常量池的内存优化
- 敏感信息的安全保障
- 多线程环境下的无锁并发
- 哈希集合中的高效缓存
⚠️ 虽然不可变性带来了一些“看似不方便”的地方(比如频繁拼接要用 StringBuilder
),但权衡之下,这是 Java 设计中最成功的决策之一。
下次面试官再问“为什么 String
不可变”,别只答“因为安全”,要把这四点(池、安全、同步、哈希)都拎出来,简单粗暴,稳稳拿捏。