Java学习随笔:String 为什么被设计成不可变(2026-10-08)
刚开始学 Java 时,我知道 String 是不可变的,但并不知道“不可变”到底意味着什么。真正让我有感觉的,是一次给字符串拼接之后重新赋值,原来的变量看起来“变了”。其实变的只是引用,字符串对象本身从头到尾都没有被修改过。理解这一点,是理解 String 很多特性的起点。
String 内部用一个字符数组来保存内容,而且这个数组一旦确定就不会再被改动,所有看起来会“改变”字符串的方法,实际上都是返回了一个新的字符串对象。比如拼接、截取、替换,都会创建新对象,原来的对象静静地留在常量池或者堆里。这带来一个直接的好处:字符串可以被安全地共享。多个变量指向同一个字符串对象时,谁都不担心别人会把它改掉,因为这根本做不到。
不可变带来的第一个好处是线程安全。一个对象如果状态永远不会变,那无论多少个线程同时读它,都不会出现数据竞争。所以 String 天然就是可以跨线程安全使用的,不需要任何同步措施。这一点在多线程环境里非常宝贵,因为共享可变对象恰恰是并发 bug 的主要来源之一。
第二个好处是可以安全地作为哈希键。HashMap 依赖键的哈希值来定位元素,如果键的内容在使用过程中变了,哈希值也就跟着变,那么原本存进去的元素可能再也找不到。String 被设计成不可变,它的哈希值就能被缓存起来,既保证了稳定性,又省去了重复计算的开销。这也是为什么字符串经常被用作缓存和映射的键。
第三个好处是安全性。字符串在很多地方承载着敏感信息,比如文件路径、数据库连接串、类名。如果这些字符串可以被随意篡改,就可能带来严重的问题。类加载器会根据类名去加载字节码,如果类名在传递过程中被人改掉,程序的行为就完全失控了。不可变性让这类信息在被读取之后无法被偷偷修改。
当然,不可变也意味着代价。频繁拼接字符串会产生大量临时对象,这也是为什么在循环里拼接要改用 StringBuilder。StringBuilder 内部是一个可变的字符数组,拼接时只做扩容和拷贝,不会每次都产生新对象;需要最终结果时,再调用一次 toString 把它固化成不可变的字符串。这种“可变工具加不可变结果”的组合,是 Java 里很常见的一种分工。
理解了不可变的动机之后,再看 String、包装类型这些类为什么都被设计成 final,就顺理成章了。它们并不是为了限制我们,而是为了换取共享、安全和稳定。很多时候,一个看起来“不方便”的设计,背后保护的恰恰是最容易被忽略的东西。

浙公网安备 33010602011771号