我们都知道String在Java中是不可变的,但是检查下面的代码:

String s1 = "Hello World";  
String s2 = "Hello World";  
String s3 = s1.substring(6);  
System.out.println(s1); // Hello World  
System.out.println(s2); // Hello World  
System.out.println(s3); // World  

Field field = String.class.getDeclaredField("value");  
field.setAccessible(true);  
char[] value = (char[])field.get(s1);  
value[6] = 'J';  
value[7] = 'a';  
value[8] = 'v';  
value[9] = 'a';  
value[10] = '!';  

System.out.println(s1); // Hello Java!  
System.out.println(s2); // Hello Java!  
System.out.println(s3); // World  

为什么这个程序是这样运行的?为什么s1和s2的值变了,而s3的值不变?


当前回答

这里有两个问题:

字符串真的是不可变的吗? 为什么s3没有改变?

第一点:除了ROM,在你的计算机中没有不可变的内存。现在甚至ROM有时也是可写的。总有一些代码(无论是绕过托管环境的内核代码还是本机代码)可以写入内存地址。所以,在“现实”中,它们不是绝对不变的。

对于第2点:这是因为substring可能分配了一个新的字符串实例,这可能是复制数组。substring有可能以不复制的方式实现,但这并不意味着它会这样做。这涉及到权衡。

例如,应该持有对reallyLargeString.substring(reallyLargeString. substring)的引用。长度- 2)导致大量的内存保持活跃,还是只有几个字节?

这取决于substring如何实现。深度复制将保留较少的活动内存,但运行速度会稍微慢一些。浅拷贝可以保留更多的内存,但速度更快。使用深度拷贝还可以减少堆碎片,因为字符串对象及其缓冲区可以分配在一个块中,而不是两个单独的堆分配。

在任何情况下,看起来您的JVM都选择对子字符串调用使用深度拷贝。

其他回答

字符串本质上是不可变的,因为没有方法来修改字符串对象。 这就是他们引入StringBuilder和StringBuffer类的原因

根据池化的概念,所有包含相同值的String变量将指向相同的内存地址。因此,s1和s2都包含相同的" Hello World "值,将指向相同的内存位置(例如M1)。

另一方面,s3包含“World”,因此它将指向不同的内存分配(比如M2)。

所以现在所发生的是S1的值被改变了(通过使用char[]值)。因此,由s1和s2指向的内存位置M1的值被改变了。

因此,内存位置M1被修改,导致s1和s2的值发生变化。

但是位置M2的值保持不变,因此s3包含相同的原始值。

字符串是不可变的*,但这只意味着你不能使用它的公共API更改它。

您在这里所做的是绕过常规API,使用反射。同样地,你可以改变枚举的值,改变Integer自动装箱中使用的查找表等等。

s1和s2改变值的原因是,它们都指向同一个被合并的字符串。编译器执行此操作(如其他答案所述)。

s3没有这样做的原因实际上让我有点惊讶,因为我认为它会共享值数组(在Java 7u6之前的早期版本中是这样的)。但是,查看String的源代码,我们可以看到子字符串的值字符数组实际上是复制的(使用Arrays.copyOfRange(..))。这就是它不变的原因。

你可以安装一个SecurityManager,避免恶意代码做这样的事情。但是请记住,有些库依赖于使用这种反射技巧(通常是ORM工具,AOP库等)。

*)我最初写的字符串不是真正的不可变,只是“有效的不可变”。在String的当前实现中,这可能会引起误解,其中值数组确实被标记为private final。不过,仍然值得注意的是,在Java中没有办法将数组声明为不可变的,因此必须注意不要在类之外公开它,即使使用适当的访问修饰符。


由于这个话题似乎非常受欢迎,这里有一些建议的进一步阅读:Heinz Kabutz在JavaZone 2009上的Reflection Madness演讲,它涵盖了OP中的许多问题,以及其他的反思……嗯…疯狂。

它涵盖了为什么这有时是有用的。以及为什么,大多数时候,你应该避免它。: -)

您正在使用反射来规避String的不可变性——这是一种“攻击”形式。

你可以创建很多这样的例子(比如你甚至可以实例化一个Void对象),但这并不意味着String不是“不可变的”。

在一些用例中,这种类型的代码可能会被用于您的优势,并且是“良好的代码”,例如在尽可能早的时候(在GC之前)从内存中清除密码。

根据安全管理器的不同,您可能无法执行代码。

String是不可变的,但是通过反射你可以改变String类。您只是实时地将String类重新定义为可变的。如果需要,可以将方法重新定义为公共的、私有的或静态的。