基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

为什么说String是不可变的

知识点图片

本文讲解了Java String的不可变性:一旦创建,其值无法改变。这种设计是为实现字符串常量池、保证线程安全和系统安全性,并使其成为可靠的哈希键。

这是一个非常经典的Java面试题和核心概念。下面我将从“是什么”、“如何实现”和“为什么这么设计”三个方面来详细解释为什么Java中的String对象是不可变的。


1. 是什么:String的不可变性是什么意思?

不可变(Immutable)意味着,当一个String对象被创建后,它内部的字符序列(the sequence of characters)就永远不能被改变了。

你可能会有疑问:“可是我明明可以这样做啊?”

java
String s = "Hello";
s = s + " World";
System.out.println(s); // 输出 "Hello World"

看起来 s 的值被改变了,但实际上,这是一种错觉。在内存中发生的事情是:

  1. JVM首先创建了一个String对象,值为 "Hello",并让引用 s 指向它。
  2. 当执行 s + " World" 时,JVM 创建了一个全新的String对象,其值为 "Hello World"。
  3. 然后,JVM将引用 s 指向这个新的对象
  4. 原来的 "Hello" 对象如果没有任何其他引用指向它,就会在未来的某个时候被垃圾回收器(Garbage Collector)回收。

所以,不是原始的String对象被修改了,而是引用s的指向被改变了。所有看起来像修改String的方法(如 concat(), substring(), replace(), toUpperCase())实际上都是返回一个新的String对象,而不会动原来的对象。


2. 如何实现:Java是如何保证String的不可变性的?

String的不可变性不是一个口头约定,而是通过代码设计强制保证的。主要有以下几点(以OpenJDK为例):

  1. final修饰的类
    String类被声明为 public final class Stringfinal关键字意味着这个类不能被任何其他类继承。这就杜绝了有人通过创建一个子类,并重写父类的方法来破坏其不可变性的可能性。

  2. private final的内部存储
    String内部用于存储字符数据的数组(在较早的JDK中是char[] value,在JDK 9以后为了节省空间,变成了byte[] value)被声明为 privatefinal

    • private:意味着除了String类自身,外部任何代码都无法直接访问这个数组,无法修改它的内容。
    • final:意味着这个数组的引用一旦在对象构造时被赋值,就不能再指向另一个数组。
  3. 没有提供任何修改内部状态的公共方法
    String类对外暴露的所有方法中,没有任何一个方法可以直接修改内部的value数组。所有需要返回修改后字符串的方法,都是创建一个新的String对象。


3. 为什么:为什么要这样设计?

String设计为不可变带来了巨大的好处,主要体现在以下几个方面:

1. 字符串常量池(String Pool)的需要

这是最核心的原因之一。JVM为了提升性能和减少内存开销,设计了字符串常量池。当你创建一个字符串字面量时(如 String s = "Java";),JVM会先在常量池中查找是否存在值为"Java"的字符串。

  • 如果存在,就直接返回该对象的引用。
  • 如果不存在,就在常量池中创建一个新的"Java"对象,并返回其引用。
java
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2); // 输出 true

因为s1s2都指向常量池中同一个对象,所以 == 比较结果为true

试想一下: 如果String是可变的,那么当s1修改了字符串的值(例如 s1.changeValue("Python");),那么s2的值也会无意中被改成"Python",这将导致程序逻辑的严重混乱。只有当字符串是不可变的,常量池的共享机制才能安全、可靠地工作。

2. 线程安全(Thread Safety)

因为String对象的状态无法被改变,所以它天生就是线程安全的。多个线程可以同时读取同一个String对象,而不用担心任何一个线程会修改它,也就不需要任何同步(如synchronized)操作。这大大简化了在多线程环境下的编程。

3. 安全性(Security)

在很多场景下,字符串被用来存储敏感信息,如数据库连接的URL、用户名、密码、文件路径等。如果String是可变的,可能会引发严重的安全漏洞。

举个例子:
假设一个函数需要检查用户是否有权限访问某个文件。

java
// 假设String是可变的
void accessFile(String filePath) {
    // 1. 在这里检查用户是否有权限访问 filePath, 比如 "/user/data/report.pdf"
    checkPermission(filePath);

    // 2. 在检查通过后,到真正打开文件前,如果有其他恶意线程修改了filePath的值
    // 比如 filePath.changeTo("/etc/passwd"); 

    // 3. 打开文件,此时打开的是一个未授权的敏感文件!
    open(filePath); 
}

由于String是不可变的,传递给checkPermission方法的字符串和传递给open方法的字符串保证是同一个,无法在中途被篡改,从而保证了安全性。

4. 适合用作HashMap的键(Key)

HashMap等基于哈希表的集合类,要求用作键(Key)的对象必须是不可变的,或者至少其hashCode()equals()方法所依赖的字段是不可变的。

  • 当一个对象放入HashMap时,会根据其hashCode()计算存储位置。
  • 如果之后这个对象的内容改变了,它的hashCode()可能也会随之改变。
  • 这样,当你再去HashMap中查找这个键时,会因为hashCode()不同而找不到原来的值,导致数据丢失。

String的不可变性保证了其hashCode()一旦计算出来就不会再变,因此可以稳定地作为HashMap的键。String还对hashCode做了缓存,第一次调用hashCode()时会计算并缓存起来,后续调用直接返回缓存值,提高了性能。

总结

String的不可变性是Java语言设计中的一个重要基石。它通过final类、private final内部数组和无修改方法等手段实现,并带来了常量池优化、线程安全、系统安全以及作为哈希键的可靠性等诸多好处。

当你需要频繁拼接或修改字符串时,为了避免创建大量临时对象带来的性能开销,应该使用StringBuilder(非线程安全,性能高)或StringBuffer(线程安全,性能稍低)。它们是可变的字符序列,专门用于高效的字符串构建。

00:00
00:00