杰瑞科技汇

为何Java String要设计成不可变?

什么是 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

执行过程解析:

  1. String s1 = "Hello";:JVM 在字符串常量池中创建了一个值为 "Hello" 的 String 对象,s1 引用它。
  2. String s2 = s1;s2 也引用了同一个 "Hello" 对象。
  3. s1 = s1 + " World";:这里发生了关键操作。
    • 运算符会创建一个新的 StringBuilder 对象。
    • StringBuilder 调用 append("Hello")append(" World")
    • StringBuilder 调用 toString() 方法,创建了一个全新的 String 对象为 "Hello World"。
    • s1 这个引用变量被重新指向了这个新创建的 "Hello World" 对象。
  4. 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 经常被用作 HashMapHashSet 的键,对象的哈希值通常是基于其内容计算的。String 内容可变,那么它的哈希值也会随之改变,这将导致在哈希表中无法再找到它(因为它已经被存放在基于旧哈希值的桶里),从而破坏数据结构的完整性。

性能优化

虽然每次修改都创建新对象听起来可能很低效,但不可变性带来的优化远超于此。

  • 哈希码缓存:因为 String 不可变,所以它的哈希值在第一次计算后可以被缓存起来,之后每次调用 hashCode() 方法时,无需重新计算,直接返回缓存值即可,这对于 HashMap 等依赖哈希码的集合来说,性能提升非常显著。
  • 作为不变对象,可以被自由地共享和重用,如第1点所述,这减少了内存中对象的创建数量。

String 不可变性的实现机制

String 类是如何保证其不可变性的?主要通过以下几个设计:

  1. 类声明为 final

    public final class String ...

    这意味着 String 类不能被继承。String 可被继承,那么子类就可以重写其方法(如 substring(), replace() 等)来改变其行为,从而破坏不可变性。

  2. 内部字符数组声明为 private final

    public 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 类中的所有方法都遵守了不修改这个数组的约定,从而保证了不可变性。
  3. 没有提供任何修改字符串内容的方法 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 设计为不可变类是一个经过深思熟虑的、权衡了性能、内存、安全性和并发性后做出的优秀工程决策,虽然它在某些场景下会创建新对象,但其带来的整体好处远大于这一点“成本”。

分享:
扫描分享到社交APP
上一篇
下一篇