在 Java 中,String 类是不可变的,即 String 中的对象是不可变的。
区别对象和对象的引用
对于 Java 初学者, 对于 String 是不可变对象总是存有疑惑。例如如下代码:
1 | String s = "ABCabc"; |
打印出的结果为:
1 | s = ABCabc |
首先创建一个 String 对象 s,然后让 s 的值为“ABCabc”, 然后又让 s 的值为“123456”。从打印结果可以看出,s 的值确实改变了,那么为什么说 String 对象是不可变的呢?其实这里存在一个误区:s 只是一个 String 对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个 4 字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。
也就是说,s 只是一个引用,它指向了一个具体的对象,当s=“123456”;
这句代码执行过之后,又创建了一个新的对象“123456”, 而引用 s 重新指向了这个新的对象,原来的对象“ABCabc”还在内存中存在,并没有改变。内存结构如下图所示:
不可变类
如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。即不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能再指向其它的对象,引用类型指向的对象的状态也不能改变。
《Effective Java》中第 15 条使可变性最小化中对不可变类的解释:
不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并且在对象的整个生命周期内固定不变。为了使类不可变,要遵循下面五条规则:
- 不要提供任何会修改对象状态的方法。
- 保证类不会被扩展。一般的做法是让这个类称为
final
的,防止子类化,破坏该类的不可变行为。- 使所有的域都是
final
的。- 使所有的域都成为私有的。防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。
- 确保对于任何可变性组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。
在 Java 平台类库中,包含许多不可变类,例如String
, 基本类型的包装类,BigInteger
, BigDecimal
等等。综上所述,不可变类具有一些显著的通用特征:类本身是final
修饰的;所有的域几乎都是私有final
的;不会对外暴露可以修改对象属性的方法。通过查阅 String 的源码,可以清晰的看到这些特征。
源码
通过查看 Java 中 String 的源码,如下所示:
1 | public final class String implements Serializable, Comparable<String>, CharSequence { |
首先,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 | public static void testReflection() throws Exception { |