String 为什么是不可变的?

String 类中使用 final 关键字修饰字符数组来保存字符串,所以String 对象是不可变的。

1
2
3
4
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char[] value;
//...
}

🐛 修正:我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。

String 真正不可变有下面几点原因:

1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

相关阅读:如何理解 String 类型值的不可变? - 知乎提问open in new window

在 Java 9 之后,StringStringBuilderStringBuffer 的实现改用 byte 数组存储字符串。

1
2
3
4
5
6
7
8
9
10
public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
// @Stable 注解表示变量最多被修改一次,称为“稳定的”。
@Stable
private final byte[] value;
}

abstract class AbstractStringBuilder implements Appendable, CharSequence {
byte[] value;

}

Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?

新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。

String不可变的意义

String 类在 Java 中是一个非常重要的类,它表示字符序列,并且设计为不可变的(immutable)。String 类底层使用 final char[] value 数组来存储字符数据,这意味着一旦创建了一个 String 对象,它的内容就不能再被更改。不可变性带来了多个好处:

  1. 线程安全
  • 不可变性保证了 String 对象在多线程环境中是安全的,因为它的内容不会改变。这意味着多个线程可以共享一个 String 对象,而不用担心数据一致性问题。
  1. 节省内存
  • Java 运行时会在字符串常量池中存储字符串字面量,如果存在相同的字符串字面量,就会共享相同的内存空间。这种共享减少了重复字符串的存储,节省了内存资源。例如,String s1 = "abc"; String s2 = "abc"; 在这种情况下,s1s2 引用的是同一个对象。
  1. 简化编程模型
  • 不可变性简化了编程模型,因为一旦创建了 String 对象,就不需要担心后续的操作会改变它的内容。这有助于减少程序中的错误。
  1. 哈希码稳定性
  • String 类重写了 hashCode 方法,使其返回值依赖于字符串的内容。由于 String 是不可变的,其哈希码在对象创建时计算一次即可,之后不会改变。这对于 HashMap 或其他依赖哈希码的数据结构非常重要,因为哈希码的变化会导致这些数据结构无法正常工作。
  1. 方便作为键使用
  • 由于 String 的不可变性,它可以安全地作为 HashMap 或其他集合框架中的键使用。当 String 作为键时,其内容不会改变,因此总是可以正确地找到对应的值。
  1. 简化内部优化
  • JVM 能够对不可变的 String 做更多的优化,因为它们的内容永远不会改变。例如,JIT 编译器可以更自由地缓存结果,提高性能。
  1. 简化序列化过程
  • 由于 String 对象是不可变的,它们在序列化和反序列化过程中不会出现问题,因为对象的状态不会发生变化。