我们都知道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的值不变?
[免责声明,这是一个故意固执己见的回答风格,因为我觉得一个更“不要在家里这样做,孩子们”的回答是有保证的]
sin是line field.setAccessible(true);它说通过允许访问私有字段来违反公共API。这是一个巨大的安全漏洞,可以通过配置一个安全管理器来锁定。
The phenomenon in the question are implementation details which you would never see when not using that dangerous line of code to violate the access modifiers via reflection. Clearly two (normally) immutable strings can share the same char array. Whether a substring shares the same array depends on whether it can and whether the developer thought to share it. Normally these are invisible implementation details which you should not have to know unless you shoot the access modifier through the head with that line of code.
依赖这些细节并不是一个好主意,因为如果不违反使用反射的访问修饰符就无法体验这些细节。该类的所有者只支持普通的公共API,并且可以在将来自由地进行实现更改。
Having said all that the line of code is really very useful when you have a gun held you your head forcing you to do such dangerous things. Using that back door is usually a code smell that you need to upgrade to better library code where you don't have to sin. Another common use of that dangerous line of code is to write a "voodoo framework" (orm, injection container, ...). Many folks get religious about such frameworks (both for and against them) so I will avoid inviting a flame war by saying nothing other than the vast majority of programmers don't have to go there.
补充@haraldK的答案-这是一个安全黑客,可能会导致应用程序的严重影响。
首先要修改存储在字符串池中的常量字符串。当string被声明为string s = "Hello World";时,它被放置到一个特殊的对象池中以供进一步重用。问题是编译器将在编译时放置对修改后版本的引用,一旦用户在运行时修改了存储在此池中的字符串,代码中的所有引用将指向修改后的版本。这将导致以下错误:
System.out.println("Hello World");
将打印:
Hello Java!
当我在这样危险的字符串上实现繁重的计算时,我遇到了另一个问题。在计算过程中出现了一个bug,大约发生了100万分之一的概率,这使得结果不确定。我能够通过关闭JIT来发现问题——我总是得到相同的结果,关闭JIT。我猜原因是这个字符串安全黑客破坏了一些JIT优化契约。
可见性修饰符和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没有奇怪的行为。