我们都知道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都选择对子字符串调用使用深度拷贝。
补充@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,因此它不会更改。