我们都知道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的值不变?


当前回答

你用的是哪个版本的Java ?从Java 1.7.0_06开始,Oracle改变了String的内部表示形式,尤其是子字符串。

引用自Oracle Tunes Java的内部字符串表示:

在新的范例中,String offset和count字段已被删除,因此子字符串不再共享底层char[]值。

有了这个变化,它可能在没有反思的情况下发生(??)

其他回答

字符串不可变性是从接口的角度来看的。您使用反射绕过接口,直接修改String实例的内部结构。

s1和s2都被改变了,因为它们都被分配给了同一个“intern”String实例。您可以从这篇关于字符串相等和实习的文章中找到更多关于这部分的内容。您可能会惊讶地发现,在示例代码中,s1 == s2返回true!

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

您正在使用反射来访问字符串对象的“实现细节”。不可变性是对象的公共接口的特性。

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

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

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

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

可见性修饰符和final(即不可变性)不是针对Java中的恶意代码的度量;它们仅仅是防止出现错误并使代码更具可维护性的工具(这是系统的一大卖点)。这就是为什么您可以通过反射访问内部实现细节,例如字符串的支持字符数组。

你看到的第二个效果是所有的字符串都改变了,而看起来你只改变了s1。它是Java字符串字面量的一个特定属性,它们被自动存储,即缓存。两个具有相同值的String字面值实际上是同一个对象。当你用new创建一个String时,它不会自动被实习生,你也不会看到这个效果。

#substring until recently (Java 7u6) worked in a similar way, which would have explained the behaviour in the original version of your question. It didn't create a new backing char array but reused the one from the original String; it just created a new String object that used an offset and a length to present only a part of that array. This generally worked as Strings are immutable - unless you circumvent that. This property of #substring also meant that the whole original String couldn't be garbage collected when a shorter substring created from it still existed.

在当前的Java和当前版本的问题中,#substring没有奇怪的行为。