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


当前回答

补充@haraldK的答案-这是一个安全黑客,可能会导致应用程序的严重影响。

首先要修改存储在字符串池中的常量字符串。当string被声明为string s = "Hello World";时,它被放置到一个特殊的对象池中以供进一步重用。问题是编译器将在编译时放置对修改后版本的引用,一旦用户在运行时修改了存储在此池中的字符串,代码中的所有引用将指向修改后的版本。这将导致以下错误:

System.out.println("Hello World"); 

将打印:

Hello Java!

当我在这样危险的字符串上实现繁重的计算时,我遇到了另一个问题。在计算过程中出现了一个bug,大约发生了100万分之一的概率,这使得结果不确定。我能够通过关闭JIT来发现问题——我总是得到相同的结果,关闭JIT。我猜原因是这个字符串安全黑客破坏了一些JIT优化契约。

其他回答

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

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

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

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

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

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

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

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

可见性修饰符和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没有奇怪的行为。