Java 中 String 为什么是不可变的?
上次更新:2024 年 1 月 8 日
1. 简介
在 Java 中,字符串是不可变的。一个在面试中经常被问到的问题是“为什么 Java 中的 String 被设计成不可变的?”
Java 的创建者 James Gosling 在一次采访中被问到应该在什么时候使用不可变对象,他回答说
只要可能,我都会使用不可变对象。
他进一步阐述了他的观点,说明了不可变性提供的特性,例如缓存、安全性、易于重用而无需复制等。
在本教程中,我们将进一步探讨 Java 语言设计者决定保持 String 不可变的原因。
2. 什么是不可变对象?
不可变对象是 在完全创建后其内部状态保持不变的对象。这意味着一旦对象被赋值给一个变量,我们就不能通过任何方式更新引用或改变内部状态。
我们有一篇单独的文章详细讨论了不可变对象。有关更多信息,请阅读 Java 中的不可变对象 文章。
3. 为什么 Java 中的 String 是不可变的?
保持这个类不可变的主要好处是缓存、安全性、同步和性能。
让我们讨论一下这些是如何运作的。
3.1. 介绍 String 池
String 是使用最广泛的数据结构。缓存 String 字面量并重用它们可以节省大量的堆空间,因为不同的 String 变量指向 String 池中的同一个 对象。String 驻留池(intern pool)正是为了这个目的而服务的。
Java 字符串池是 JVM 中存储 Strings 的特殊内存区域。由于 Strings 在 Java 中是不可变的,JVM 通过在池中存储每个字面量 String 的唯一副本来优化为其分配的内存量。这个过程称为驻留(interning)
String s1 = "Hello World";
String s2 = "Hello World";
assertThat(s1 == s2).isTrue();
由于前面示例中存在 String 池,两个不同的变量指向池中的同一个 String 对象,从而节省了宝贵的内存资源。
我们有一篇专门介绍 Java String 池的文章。有关更多信息,请访问那篇文章。
3.2. 安全性
String 在 Java 应用程序中被广泛用于存储敏感信息,例如用户名、密码、连接 URL、网络连接等。它也被 JVM 类加载器在加载类时广泛使用。
因此,从总体安全角度来看,保护 String 类至关重要。例如,考虑以下简单的代码片段
void criticalMethod(String userName) {
// perform security checks
if (!isAlphaNumeric(userName)) {
throw new SecurityException();
}
// do some secondary tasks
initializeDatabase();
// critical task
connection.executeUpdate("UPDATE Customers SET Status = 'Active' " +
" WHERE UserName = '" + userName + "'");
}
在上面的代码片段中,假设我们从不可信来源收到了一个 String 对象。我们正在执行所有必要的安全检查,以检查 String 是否仅为字母数字,然后执行一些其他操作。
请记住,我们不可信的来源调用者方法仍然对这个 userName 对象有引用。
如果 Strings 是可变的,那么在我们执行更新的时候,我们无法确定我们接收到的 String,即使在执行安全检查之后,是否安全。 不可信的调用者方法仍然拥有引用,并且可以在完整性检查之间更改 String。因此,在这种情况下,我们的查询容易受到 SQL 注入的攻击。因此,可变的 Strings 可能会随着时间的推移导致安全性的降低。
也可能出现String userName 对另一个线程可见的情况,该线程可以在完整性检查之后更改其值。
通常,不变性在这种情况下起到了救援作用,因为当值不会改变时,使用敏感代码更容易操作,因为可能影响结果的操作交错情况更少。
3.3. 同步
由于String 不会被更改,因此自动使其成为线程安全的,即使从多个线程访问时也是如此。
因此,**总的来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的**,因为如果一个线程更改了值,那么它不会修改相同的对象,而是在String池中创建一个新的String。因此,Strings 对于多线程操作是安全的。
3.4. Hashcode 缓存
由于String 对象被大量用作数据结构,因此它们也被广泛用于像HashMap、HashTable、HashSet等哈希实现中。在对这些哈希实现进行操作时,hashCode() 方法会被频繁调用用于分桶。
不变性保证了Strings 的值不会改变。因此,**hashCode() 方法在String 类中被重写以实现缓存,以便在第一次hashCode() 调用期间计算并缓存哈希值,并且之后每次都返回相同的值。**
这反过来提高了使用哈希实现与String 对象进行操作时集合的性能。
另一方面,如果String 的内容在操作之后被修改,可变的Strings 会在插入和检索时产生两个不同的哈希码,从而可能在Map 中丢失值对象。
3.5. 性能
正如我们之前看到的,String 池存在是因为Strings 是不可变的。反过来,它通过节省堆内存并在与Strings操作时更快地访问哈希实现来提高性能。
由于String 是使用最广泛的数据结构,因此提高String 的性能对提高整个应用程序的性能有相当大的影响。
4. 结论
通过本文,我们可以得出结论:**Strings 是不可变的,正是为了使它们的引用可以像普通变量一样处理,并且可以在方法之间和线程之间传递,而不必担心它们指向的实际String 对象会发生更改。**
我们还了解到,可能还有其他原因促使Java 语言设计者将此类设计为不可变的。















