java - 一个Java字符串是不可变的?

  显示原文与译文双语对照的内容

我们都知道 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 

为什么这个程序像这样运行? 为什么 s1s2的值改变了,但不是 s3

时间:

String 是 immutable*,但这意味着你不能使用它的公共API来改变它。

你正在做的是绕过普通 API,使用反射。 同样,你可以更改枚举的值,更改整数自动装箱中使用的查找表。

现在,s1s2 更改值的原因是它们都引用了同一个已经插入的字符串。 编译器执行这里( 就像其他答案所提到的) 。

实际上 s3 确实不的理由的对我有点惊讶,因为我认为这将共享( 在早期版本的Java 之前,在 Java 7 u6之前) value 数组。 但是,查看 String的源代码,我们可以看到,子字符串的value 字符数组实际上被复制为( 使用 Arrays.copyOfRange(..) ) 。 这就是为什么它没有改变。

你可以安装 SecurityManager,以避免恶意代码执行此类操作。 但是记住一些库依赖于使用这种反射技巧( 典型的ORM工具,AOP库等) 。

*) 我最初写的String 不是真正不变的,只是"有效不可变"。 这在当前的String 实现中可能会产生误导,其中 value 数组确实被标记为 private final 。 但是,值得注意的是,在Java中无法声明数组为不可变的,所以必须小心,不要在它的类之外公开它,即使使用适当的访问修饰符。


由于本主题看起来非常流行,下面是一些建议的进一步阅读: 其他反射。., 海因茨反射kabutz随意性的谈话从 JavaZone 2009 OP,along.中,我将讨论很多的这些问题 呃。。疯狂。

它涵盖了为什么有时会有用。 为什么,大多数时候,你应该避免。 :- )

在Java中,如果两个字符串基元变量用相同的文本初始化,它们为两个变量指定相同的引用:


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

initialization

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

sub string

使用反射访问字符串时,会得到实际指针:


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

因此,改变会改变保存它指针的字符串,但由于 substring() 被创建了一个新字符串,它不会改变。

change

你使用反射来规避字符串的不变性- 它是"攻击"的一种形式。

你可以像这个( 例如你甚至可以实例化 Void 对象 ) 创建很多示例,但这并不意味着字符串不是"不可变的"。

在尽可能早的时刻( 在GC之前), 有应用中才能知道这种类型的代码可能被使用作为你的优势,被"好编码",例如清除密码来自内存。

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

可见性修饰符和 final ( 例如 。 不变性) 不是对Java中恶意代码的度量;它们只是防止错误和使代码更易维护的工具。 这就是为什么你可以访问内部实现细节,比如 String的via反射char数组。

你看到的第二个效果是所有 String的改变,看起来你只是改变了 s1 。 它是一个特定的Java字符串的属性,它们被自动地自动插入,换句话说,缓存。 具有相同值的两个字符串实际上是相同的对象。 当你用 new 创建字符串时,它将不会自动被锁定,并且不会看到这里效果。

#substring 直到最近( Java 7 u6 ) 以类似的方式工作,这将解释你的问题原始版本中的行为。 它没有创建一个新的支持char数组,而是重用了原始字符串中的一个;它只创建了一个使用偏移量和长度的新字符串对象,只显示该数组的一部分。 这通常是字符串是不可变的- 除非你避开。 #substring的这个属性也意味着当一个较短的子字符串仍然存在时,整个原始字符串不能被垃圾收集。

在当前的Java和当前版本的问题中,#substring 没有奇怪的行为。

字符串不变性来自界面透视图。 你正在使用反射绕过接口并直接修改字符串实例的内部。

s1s2 都被更改,因为它们都被分配给同一个"实习生"字符串实例。 你可以从中了解更多关于这个部分的内容本文关于字符串等同性和 interning 。 你可能会惊奇地发现,在示例代码中,s1 == s2 返回 true

这里有 2个问题:

  1. 字符串真的不可变?
  2. 为什么s3没有改变?

指向 1: 除了 ROM,你的计算机中没有不可变的内存。 现在ROm有时也可以写。 总是有些代码可以写入到你的内存地址。 因此,在"真实"。- 不它们不是绝对不可变的。

指向 2: 这是因为子字符串可能正在分配一个新的字符串实例,这可能会复制数组。 可以实现子字符串,这样它就不会执行复制,但这并不意味着。 涉及到交易抵销。

例如应该保留一个对 reallyLargeString.substring(reallyLargeString.length - 2) 使大量的内存保持活动状态,或者只有几个字节?

这取决于子字符串是如何实现的。 深度复制将减少内存的存活,但运行速度会稍微慢一些。 浅表复制将保留更多的内存,但更快。 使用深层副本也可以减少堆碎片,因为字符串对象和它的缓冲区可以在一个块中分配,而不是。 2独立的堆分配。

在任何情况下,看起来你的JVM都选择了对子字符串调用使用深层副本。

在app,给添加到 @haraldK's 回答- 这是安全 hack 这可能会导致一个严重的impact.

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


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

将打印:

 
Hello Java!

 

当我在这样的风险字符串上实现大量计算时,我遇到了另一个问题。 时出现 Bug,并针对d 好象 1超出时间 1000000在该计算结果把结果 undeterministic 。 JIT,一旦你off,我又找到解决办法,以关掉电源,防止 JIT - 我总是收到同样的result. 我猜原因是这个字符串安全 hack 破坏了一些JIT优化契约。

...