我们都知道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实际上没有更改的原因是,在Java中,当您执行子字符串时,子字符串的值字符数组会在内部复制(使用Arrays.copyOfRange())。

s1和s2是相同的,因为在Java中它们都指向同一个被合并的字符串。这是在Java中设计的。

其他回答

字符串在JVM堆内存的永久区域中创建。是的,它确实是不可变的,在创建之后就不能更改。 因为在JVM中,有三种类型的堆内存: 1. 年轻的一代 2. 老的代 3.永久的一代。

当创建任何对象时,它将进入年轻代堆区域和为字符串池保留的永久代区域。

以下是更多细节,你可以从中获取更多信息: 垃圾收集在Java中的工作原理。

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

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

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

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

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

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

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

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

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

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

在Java中,如果两个字符串基元变量被初始化为相同的字面值,它会将相同的引用赋给这两个变量:

String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true

这就是比较返回true的原因。第三个字符串是使用substring()创建的,它创建了一个新的字符串,而不是指向相同的字符串。

当你使用反射访问一个字符串时,你会得到实际的指针:

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

因此,更改this将更改持有指向它的指针的字符串,但由于substring()使用新字符串创建了s3,因此它不会更改。