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


当前回答

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

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

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

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

其他回答

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

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

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

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

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

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

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

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

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

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

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

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

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

将打印:

Hello Java!

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

在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,因此它不会更改。