首先,我们要知道在 Java 的世界里,String
类的不可变性(Immutability)是一项深思熟虑的核心设计决策。理解其背后的原因,不仅是应对面试的关键,更是深入理解 Java 内存模型、安全性和性能优化的基石。
简单来说,字符串不可变意味着一旦一个 String
对象被创建,它的值就不能被改变。任何看似修改字符串的操作(如 concat()
, toUpperCase()
),实际上都是创建了一个全新的 String
对象。
下面我们从设计原因、实现机制、优缺点和实际应用等方面来了解为什么在 Java 中字符串是不可变的。
一、 如何实现不可变性?
查看 String
类的源码,你会发现三个关键设计:
final
类 :String
类被声明为final
,防止被继承。这意味着没有子类可以重写其方法从而破坏不可变行为。private final
的字符数组 :字符串的数据实际存储在一个private final char value[]
(或 JDK 9 后的byte[]
)中。关键字private
确保了外部无法直接访问这个数组,final
确保了该数组的引用不可更改(但请注意,final
只能保证引用不变,并不能保证数组内容不变)。- 无修改内容的公共方法 :
String
类没有任何会修改内部字符数组内容的公共方法。所有像substring()
,replace()
,trim()
等方法,如果需要修改内容,都会在内部创建一个新的String
对象并返回。

二、为什么设计成不可变的?
主要体现在安全性、性能和多线程方面。
1. 安全性 (Security)
字符串被广泛用于存储敏感信息,如用户名、密码、网络连接地址、文件路径等。Java 类加载器、数据库驱动程序等大量底层实现都依赖字符串。
- 示例 :如果字符串是可变的,恶意代码可能在你进行权限检查后,通过修改其引用的字符串值(例如将文件路径从
"/tmp/file"
改为/etc/passwd
)来提升权限或访问敏感资源。不可变性从根本上杜绝了这种威胁。
2. 线程安全 (Thread Safety)
不可变对象天生就是线程安全的。因为它们的状态无法改变,所以可以被多个线程共享和访问,而无需任何同步(synchronization)开销。不存在"脏读"或"写冲突"的问题。这极大地提高了程序的性能和可靠性。
3. 性能优化:字符串常量池 (String Pool)
这是不可变性带来的最著名的性能好处。
- 机制 :Java 在堆内存中开辟了一块特殊的存储区域,称为"字符串常量池"(String Pool)。当创建一个字符串字面量(如
String s = "Hello";
)时,JVM 会首先在池中查找是否存在相同值的字符串。如果存在,则返回现有对象的引用;如果不存在,则在池中创建一个新对象。 - 依赖不可变性 :这种重用(Reuse)策略完全依赖于字符串的不可变性。如果字符串可变,那么共享引用会导致一个地方的修改影响到其他看似无关的代码,这是灾难性的。正因为不可变,共享才绝对安全。

4. 哈希码缓存 (HashCode Caching)
字符串是不可变的,所以它的哈希码(hashCode()
)也是不变的。这使得 String
是作为 HashMap
或 HashSet
的键的完美候选。
- 机制 :
String
类内部有一个int hash
的私有字段来缓存第一次计算得到的哈希值。因为内容不会变,所以这个哈希值只需要计算一次,之后每次调用hashCode()
方法时直接返回这个缓存值即可。这极大地提高了散列集合(如HashMap
)的性能,因为频繁的get()
和put()
操作需要频繁计算键的哈希值。
三、潜在的"缺点"与解决方案
不可变性并非没有代价,最主要的问题就是:频繁的字符串修改(如循环拼接)会产生大量中间垃圾对象,降低性能并增加 GC 压力。
解决方案:Java 提供了两个可变的(mutable)字符串类来解决这个问题:
StringBuilder
:非线程安全,但性能最高。适用于单线程场景下的字符串拼接。StringBuffer
:线程安全(其方法使用synchronized
修饰),但性能稍低。适用于多线程场景下的字符串拼接。
最佳实践 :在循环体内或需要频繁修改字符串的地方,务必使用 StringBuilder
。

最后↓↓↓↓

Java 将 String
设计为不可变,是一项极具远见的决策。它通过牺牲小部分场景下的修改效率,换来了全局性的安全、稳定和性能提升,是工程学上"权衡"(Trade-off)艺术的典范。