⭐ 推荐:社区规则条款 V1.0

为什么 Java 的 String 类被设计成不可变的?

周末
周末 正式会员超兽战士
发布于 2026-10-11 06:45 ·1 浏览 ·0 回复

学完这篇你能得到:能用代码和源码验证 Java 的 String 不可变,并说清它为什么这样设计、什么时候该改用 StringBuilder。

第一步:先确认 String 真的不可变

这一步要做什么:写一段最小测试代码,做完得到的结果是:你能看到“修改字符串”的方法返回新对象,而不是改变原对象。

入口位置:安装 JDK 17 或更高版本,打开终端。新建目录和文件:

mkdir string-immutability
cd string-immutability

创建 StringImmutableDemo.java:

public class StringImmutableDemo {
    public static void main(String[] args) {
        String a = "hello";
        String b = a.toUpperCase();

        System.out.println(a);
        System.out.println(b);
        System.out.println(a == b);
        System.out.println(System.identityHashCode(a));
        System.out.println(System.identityHashCode(b));
    }
}

编译并运行:

javac StringImmutableDemo.java
java StringImmutableDemo

输出类似:

hello
HELLO
false
12345678
87654321

这说明 a 仍然是 hello,toUpperCase() 返回了新的 String 对象。

注意:这里用 == 只是为了观察对象地址,证明 a 和 b 不是同一个对象。业务代码比较字符串内容要用 equals(),不要用 ==。

第二步:打开 String 源码看关键设计

这一步要做什么:查看 JDK 里的 java.lang.String 源码,做完得到的结果是:你能看到不可变的核心实现方式:final 类、final 字段、修改方法返回新对象。

入口位置:在 IntelliJ IDEA 2023.2 中,按 Ctrl + N(macOS 是 Cmd + O),输入 java.lang.String,选择当前项目 SDK,例如 17 或 21,回车打开源码。

源码里你会看到类似结构:

public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {

    private final byte[] value;
}

String 类被 final 修饰,表示不能被继承。value 字段被 private final 修饰,表示对象创建后不能重新指向另一个数组。toUpperCase()、substring()、trim() 这类方法不会修改 value,而是创建新的 String 返回。

注意:Java 8 里 String 内部是 char[],Java 9 以后改成 byte[] 加 coder 来节省内存,但“不可变”的设计思想没有变。

第三步:理解常量池和 hashCode 缓存

这一步要做什么:观察字符串字面量和 hashCode(),做完得到的结果是:你能理解为什么不可变 String 适合放进 HashMap,也能理解字符串常量池为什么能共享对象。

在同一目录创建 StringPoolDemo.java:

public class StringPoolDemo {
    public static void main(String[] args) {
        String x = "java";
        String y = "java";
        String z = new String("java");

        System.out.println(x == y);
        System.out.println(x == z);
        System.out.println(x.equals(z));
        System.out.println(x.hashCode());
    }
}

运行:

javac StringPoolDemo.java
java StringPoolDemo

输出类似:

true
false
true
101450093

x == y 是 true,因为两个字符串字面量 "java" 指向常量池中的同一个对象。String z = new String("java") 会在堆上创建新对象,所以 x == z 是 false,但 x.equals(z) 是 true。

不可变让 String 可以安全缓存 hashCode()。第一次调用 hashCode() 后,结果可以存在对象里;以后再次调用不用重新计算。如果字符串内容能被改变,hashCode() 就会失效,放进 HashMap、HashSet 后会非常危险。

注意:intern() 可以把字符串放入常量池,但不要为了省内存到处调用。大量字符串 intern() 可能增加 GC 压力。

第四步:理解线程安全和参数传递

这一步要做什么:验证 String 作为方法参数时不会被外部修改,做完得到的结果是:你能理解不可变对象为什么适合多线程共享。

创建 StringThreadDemo.java:

public class StringThreadDemo {
    static void change(String s) {
        s = s + "-suffix";
    }

    public static void main(String[] args) {
        String a = "base";
        change(a);
        System.out.println(a);
    }
}

运行:

javac StringThreadDemo.java
java StringThreadDemo

输出:

base

change() 方法里的 s 只是局部变量。Java 参数传递是“传值”,对引用类型来说,传的是对象引用的副本。因为 String 不可变,外部拿到引用也没法改变原对象内容。

注意:不可变不是线程安全的唯一原因,但它是 String 可以安全共享的重要原因。日志、配置、URL、文件名、数据库连接参数这类地方,通常不希望被中途偷偷改掉。

第五步:知道不可变的代价和替代方案

这一步要做什么:对比循环拼接字符串的性能,做完得到的结果是:你能知道高频修改字符串时不要继续用 String,要改用 StringBuilder。

创建 StringBuilderDemo.java:

public class StringBuilderDemo {
    public static void main(String[] args) {
        String s = "";
        for (int i = 0; i < 100000; i++) {
            s += i;
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
            sb.append(i);
        }

        System.out.println(s.length());
        System.out.println(sb.length());
    }
}

运行:

javac StringBuilderDemo.java
java StringBuilderDemo

这里重点是:String 不可变,每次 s += i 都可能产生新的字符串对象。数据量小的时候不明显,循环十万次、百万次时,GC 压力和耗时都会变差。StringBuilder 内部是可变的 byte[] 或 char[],追加时通常只修改缓冲区。

注意:Java 编译器会对简单字符串拼接做一定优化,但不能依赖编译器解决所有性能问题。需要频繁拼接时,直接写 new StringBuilder() 或 new StringBuffer();多线程共享可变缓冲区时再考虑 StringBuffer,一般局部变量用 StringBuilder 就够了。

小结

  • String 不可变体现在:String 是 final 类,内部数组字段是 final,修改方法返回新对象。
  • 不可变带来好处:线程安全、适合做 HashMap key、可以缓存 hashCode()、可以安全共享字符串常量池。
  • 不可变带来代价:频繁修改字符串会产生大量临时对象。
  • 实际选择:固定文本、配置、URL、文件名用 String;循环拼接、动态文本用 StringBuilder。
版权声明:本文来自 GJ站长论坛《为什么 Java 的 String 类被设计成不可变的?》
原文链接:https://www.gj0.com/thread-1622.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~