什么是 String 不可变性?
在 Java 中,不可变性 意味着一个对象在被创建之后,其内部的状态(值)就不能被修改。
对于 String 这意味着一旦一个 String 对象被创建,你无法改变它所包含的字符序列,任何看似修改 String 的操作,实际上都是创建了一个新的 String 对象,而原始对象保持不变。
一个简单的例子
String s1 = "Hello";
String s2 = s1; // s2 和 s1 引用同一个对象
// 看似修改了 s1,但实际上发生了什么?
s1 = s1 + " World";
System.out.println("s1: " + s1); // 输出: s1: Hello World
System.out.println("s2: " + s2); // 输出: s2: Hello
执行过程解析:
String s1 = "Hello";:JVM 在字符串常量池中创建了一个值为 "Hello" 的String对象,s1引用它。String s2 = s1;:s2也引用了同一个 "Hello" 对象。s1 = s1 + " World";:这里发生了关键操作。- 运算符会创建一个新的
StringBuilder对象。 StringBuilder调用append("Hello"),append(" World")。StringBuilder调用toString()方法,创建了一个全新的String对象为 "Hello World"。s1这个引用变量被重新指向了这个新创建的 "Hello World" 对象。
- 运算符会创建一个新的
s2仍然指向最初的那个 "Hello" 对象,所以它的值没有改变。
String 对象本身没有被修改,而是被一个新对象所取代。
为什么 Java 的 String 要设计成不可变?
String 的不可变性是 Java 设计者深思熟虑的结果,主要基于以下几个关键原因:
字符串常量池 的实现
这是最重要、最直接的原因。
- 什么是字符串常量池? 它是 JVM 堆内存中的一块特殊区域,用于存储字符串字面量,当使用双引号 创建字符串时,JVM 会先检查常量池中是否已存在该字符串,如果存在,则直接返回引用;如果不存在,则创建新字符串并存入池中。
- 不可变性如何帮助池化? 因为字符串是不可变的,所以它们可以被安全地共享,多个变量可以引用同一个字符串常量,而不用担心一个变量的修改会影响其他变量,如果字符串是可变的,那么当一个引用修改了字符串内容时,其他所有引用都会受到“副作用”,这会导致池化机制崩溃和不可预测的行为。
// s1 和 s2 实际上引用的是池中同一个 "abc" 对象 String s1 = "abc"; String s2 = "abc"; System.out.println(s1 == s2); // 输出 true,因为它们是同一个对象
线程安全
在多线程环境中,不可变对象天生就是线程安全的。
- 无需同步:因为
String对象的状态在创建后永远不会改变,所以多个线程可以同时读取同一个String对象,而无需进行任何同步或加锁操作,这极大地简化了并发编程,避免了因数据竞争导致的问题。 - 可变对象的噩梦:
String是可变的,那么一个线程在读取字符串的同时,另一个线程可能会修改它,导致读取到不一致或错误的数据。
安全性
String 被广泛用于 Java 平台的安全敏感场景,例如加载类、文件路径、网络地址等。
- 防止篡改:假设
String是可变的,那么一个恶意程序可以获取到一个指向系统关键路径(如C:\Windows\System32)的String引用,然后修改其内容为恶意路径(如C:\Evil\malware.exe),由于String不可变,这种攻击在 Java 中是不可能发生的,一旦路径被设置,它就是可信的、不可更改的。 - 作为哈希表的键:
String经常被用作HashMap或HashSet的键,对象的哈希值通常是基于其内容计算的。String内容可变,那么它的哈希值也会随之改变,这将导致在哈希表中无法再找到它(因为它已经被存放在基于旧哈希值的桶里),从而破坏数据结构的完整性。
性能优化
虽然每次修改都创建新对象听起来可能很低效,但不可变性带来的优化远超于此。
- 哈希码缓存:因为
String不可变,所以它的哈希值在第一次计算后可以被缓存起来,之后每次调用hashCode()方法时,无需重新计算,直接返回缓存值即可,这对于HashMap等依赖哈希码的集合来说,性能提升非常显著。 - 作为不变对象,可以被自由地共享和重用,如第1点所述,这减少了内存中对象的创建数量。
String 不可变性的实现机制
String 类是如何保证其不可变性的?主要通过以下几个设计:
-
类声明为
finalpublic final class String ...
这意味着
String类不能被继承。String可被继承,那么子类就可以重写其方法(如substring(),replace()等)来改变其行为,从而破坏不可变性。 -
内部字符数组声明为
private finalpublic final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; // ... }private:确保外部代码无法直接访问和修改这个内部数组。final:确保一旦这个数组被初始化,它就不能再被重新赋值指向另一个数组。注意:final只能保证引用value不能指向新数组,但并不保证数组内部的元素(value[0],value[1]等)不能被修改。String类中的所有方法都遵守了不修改这个数组的约定,从而保证了不可变性。
-
没有提供任何修改字符串内容的方法
String类中没有任何public的方法可以修改其内部字符序列,所有像substring(),replace(),concat()等方法,都会返回一个新的String对象,而不是在原对象上进行修改。
可变字符串的替代方案
当你确实需要频繁修改字符串内容时(在循环中拼接大量字符串),应该使用可变的字符串类,以避免创建大量临时对象带来的性能开销。
StringBuilder:非线程安全的可变字符序列,在单线程环境下,它的性能最高,因为没有同步开销。这是绝大多数情况下的首选。StringBuffer:线程安全的可变字符序列,它的所有公共方法都使用了synchronized关键字来保证线程安全,在多线程环境下是安全的,但性能会比StringBuilder稍差。
// 高效的字符串拼接方式
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("number ").append(i);
}
String result = sb.toString();
| 特性 | 描述 | 原因/好处 |
|---|---|---|
| 不可变性 | 对象创建后,内容不能被修改。 | 核心设计原则。 |
final 类 |
不能被继承。 | 防止子类破坏不可变性。 |
private final 数组 |
存储字符的数组被私有化且引用不可变。 | 从数据结构层面保护内部状态。 |
| 无修改方法 | 所有方法都返回新对象。 | 确保外部无法修改对象状态。 |
| 优点 | 字符串常量池 (高效内存使用) 线程安全 (无需同步) 安全性 (防止恶意篡改) 性能优化 (哈希码缓存) |
设计 String 为不可变类带来的巨大好处。 |
| 替代方案 | StringBuilder (单线程, 高性能)StringBuffer (多线程, 安全但稍慢) |
在需要频繁修改字符串时使用。 |
Java 将 String 设计为不可变类是一个经过深思熟虑的、权衡了性能、内存、安全性和并发性后做出的优秀工程决策,虽然它在某些场景下会创建新对象,但其带来的整体好处远大于这一点“成本”。
