0%

Java 中的 String 为什么是不可变的

在 Java 中,String 类是不可变的,即 String 中的对象是不可变的。

区别对象和对象的引用

对于 Java 初学者, 对于 String 是不可变对象总是存有疑惑。例如如下代码:

1
2
3
4
5
String s = "ABCabc";
System.out.println("s = " + s);

s = "123456";
System.out.println("s = " + s);

打印出的结果为:

1
2
s = ABCabc
s = 123456

首先创建一个 String 对象 s,然后让 s 的值为“ABCabc”, 然后又让 s 的值为“123456”。从打印结果可以看出,s 的值确实改变了,那么为什么说 String 对象是不可变的呢?其实这里存在一个误区:s 只是一个 String 对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个 4 字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。

也就是说,s 只是一个引用,它指向了一个具体的对象,当s=“123456”;这句代码执行过之后,又创建了一个新的对象“123456”, 而引用 s 重新指向了这个新的对象,原来的对象“ABCabc”还在内存中存在,并没有改变。内存结构如下图所示:

String 真实的内存结构String 真实的内存结构

不可变类

如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。即不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能再指向其它的对象,引用类型指向的对象的状态也不能改变。

《Effective Java》中第 15 条使可变性最小化中对不可变类的解释:

不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并且在对象的整个生命周期内固定不变。为了使类不可变,要遵循下面五条规则:

  1. 不要提供任何会修改对象状态的方法。
  2. 保证类不会被扩展。一般的做法是让这个类称为final的,防止子类化,破坏该类的不可变行为。
  3. 使所有的域都是final的。
  4. 使所有的域都成为私有的。防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。
  5. 确保对于任何可变性组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。

在 Java 平台类库中,包含许多不可变类,例如String, 基本类型的包装类,BigInteger, BigDecimal等等。综上所述,不可变类具有一些显著的通用特征:类本身是final修饰的;所有的域几乎都是私有final的;不会对外暴露可以修改对象属性的方法。通过查阅 String 的源码,可以清晰的看到这些特征。

源码

通过查看 Java 中 String 的源码,如下所示:

1
2
3
4
public final class String implements Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
}

首先,String 类是通过final修饰的,满足了第二条的:类不能被拓展。

其次,在类中,最重要的一条private final byte[] value;中,我们可以看到 Java 是使用字节数组(Java9,之前的版本是采用字符 char 数组实现)来实现字符串的,并且使用了final修饰,这就是 String 为什么不可变的原因。

因为它使用了private final,导致正常情况下外界没有办法去修改它的值。这满足了第三条:使所有的域都是final的,和第四条:使所有的域都是私有的。然而仅仅这样也仍然不是万无一失的。

第一条原则是:不要提供任何会修改对象状态的方法。String 类在这点中做得很好。在 String 类中许多会对字符串进行操作的方法中,例如replaceAll()或者substring()等,其中的每一步实现都不会对value产生任何影响。即 String 类中并没有提供任何可以改变其值的方法,这比final更能确保其是不变的。

好处

《Effective Java》一书中总结了不可变类的特点:

  • 不可变类比较简单。
  • 不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以被自由地共享。
  • 不仅可以共享不可变对象,甚至可以共享它们的内部信息。
  • 不可变对象为其他对象提供了大量的构建。
  • 不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。

String 真的不可变吗

其实可以通过反射机制来破坏 String 的不可变性。可以反射出 String 对象中的 value 属性,进而改变通过获得的 value 引用改变数组的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void testReflection() throws Exception {

// 创建字符串"Hello World", 并赋给引用 s
String s = "Hello World";

System.out.println("s = " + s); // Hello World

// 获取 String 类中的 value 字段
Field valueFieldOfString = String.class.getDeclaredField("value");

// 改变 value 属性的访问权限
valueFieldOfString.setAccessible(true);

// 获取 s 对象上的 value 属性的值
char[] value = (char[]) valueFieldOfString.get(s);

// 改变 value 所引用的数组中的第 5 个字符
value[5] = '_';

System.out.println("s = " + s); // 变成 Hello_World
}