一个Javastring真的是不可变的?

我们都知道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是不可变的*但这只意味着你不能使用它的公共API来改变它。

你在这里做什么是绕过正常的API,使用反思。 同样的,你可以改变枚举的值,改变整数自动装箱中使用的查找表等。

现在, s1s2改变数值的原因是它们都指向同一个internedstring。 编译器这样做(如其他答案所述)。

s3的原因实际上让我感到有些惊讶,因为我认为它会共享value数组(在Java 7u6之前的早期版本的Java中 )。 但是,查看String的源代码,我们可以看到实际上复制了子string的value字符数组(使用Arrays.copyOfRange(..) )。 这就是为什么它不变。

您可以安装SecurityManager ,以避免恶意代码做这样的事情。 但请记住,一些库依赖于使用这些reflection技巧(通常是ORM工具,AOP库等)。

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


由于这个话题似乎非常受欢迎,这里有一些build议进一步阅读: Heinz Kabutz的反思疯狂从JavaZone 2009 谈话 ,其中涵盖了OP中的许多问题,以及其他反思…以及疯狂。

它涵盖了为什么这有时是有用的。 为什么,大多数时候,你应该避免它。 🙂

在Java中,如果两个string原始variables被初始化为相同的文字,它将同样的引用赋值给两个variables:

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

初始化

这就是比较的原因。 第三个string是使用substring()创build的,它创build一个新的string,而不是指向相同的string。

子字符串

当你使用reflection访问一个string时,你得到了实际的指针:

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

所以改成这个会改变持有指向它的string,但是由于substring()会使用新string创builds3 ,所以它不会改变。

更改

你使用reflection来规避String的不变性 – 这是一种“攻击”的forms。

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

在这种情况下,这种types的代码可能会被用于您的优势,并且是“良好的编码”,例如在尽可能早的时刻(在GC之前)从内存中清除密码 。

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

您正在使用reflection来访问string对象的“实现细节”。 不变性是对象的公共接口的特征。

可见性修饰符和最终(即不可变性)不是针对Java中的恶意代码的度量; 他们只是防止错误的工具,并使代码更易于维护(系统的一大卖点)。 这就是为什么你可以通过reflection访问内部实现的细节,比如String s的支持char数组。

你看到的第二个效果是,所有的String改变,而你看起来只改变了s1 。 Java String文字的某些属性是自动实现的,即被caching。 两个具有相同值的string实际上是相同的对象。 当你用new创build一个string时,它不会被自动实现,你不会看到这个效果。

直到最近(Java 7u6)的#substring以类似的方式工作,这将解释您的问题的原始版本中的行为。 它没有创build一个新的支持字符数组,但重用了原来的string; 它只是创build了一个新的String对象,它使用一个偏移量和一个长度来呈现该数组的一部分。 这通常作为string是不可改变的 – 除非你绕开。 #substring这个属性也意味着当从它创build的更短的子string仍然存在时,整个原始string不能被垃圾回收。

从目前的Java和你当前版本的问题#substring#substring没有奇怪的行为。

string不变性是从界面的angular度来看的。 您正在使用reflection绕过接口,并直接修改String实例的内部。

s1s2都被更改,因为它们都被分配给相同的“intern”string实例。 你可以从这篇关于string平等和实习的文章中find更多关于这个部分的内容。 您可能会惊讶地发现,在您的示例代码中, s1 == s2返回true

你正在使用哪个版本的Java? 从Java 1.7.0_06起,Oracle改变了String的内部表示,特别是子string。

引用Oracle Tunes Java的内部string表示法 :

在新的范例中,string偏移量和计数字段已被删除,所以子string不再共享底层的char []值。

有了这个变化,它可能会发生没有反思(???)。

这里真的有两个问题:

  1. string真的是不可变的吗?
  2. 为什么s3没有改变?

第1点:除ROM外,计算机中没有不可变的内存。 如今,即使ROM有时也是可写的。 总是有一些代码可以写入你的内存地址(无论是内核代码还是本地代码)。 所以,在“现实”中,不是绝对不可改变的。

要点2:这是因为substring可能是分配一个新的string实例,这可能是复制数组。 有可能以这样的方式实现子string,使其不会做副本,但这并不意味着它。 涉及权衡。

例如,应该持有对reallyLargeString.substring(reallyLargeString.length - 2)的引用,导致大量的内存被保持活着,或只有几个字节?

这取决于如何实现子string。 深拷贝将保持更less的内存活动,但会稍微慢一些。 浅拷贝将保持更多的内存活着,但会更快。 使用深层副本还可以减less堆碎片,因为string对象及其缓冲区可以分配在一个块中,而不是2个单独的堆分配。

无论如何,它看起来像你的JVMselect使用深度副本的子串调用。

要添加到@ haraldK的答案 – 这是一个安全破解,可能会导致在应用程序的严重影响。

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

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

将打印:

 Hello Java! 

当我对这种冒险的string进行大量计算时,还遇到了另一个问题。 在计算过程中发生了100万次中的1次错误,使得结果不确定。 通过closuresJIT,我能够发现问题 – closuresJIT后,我总是得到相同的结果。 我的猜测是,原因是这个String安全破解破坏了一些JIT优化合同。

根据池的概念,所有包含相同值的stringvariables将指向相同的内存地址。 因此,s1和s2都包含相同的值“Hello World”,将指向相同的内存位置(比如M1)。

另一方面,S3包含“世界”,因此它会指向不同的内存分配(如M2)。

所以现在发生的事情是S1的值正在被改变(通过使用char []值)。 因此,s1和s2指向的存储单元M1的值已经改变。

因此,存储单元M1已被修改,导致s1和s2的值发生变化。

但地点M2的价值保持不变,因此s3包含相同的原始价值。

s3实际上没有改变的原因是因为在Java中,当你做一个子string时,一个子string的值字符数组被内部复制(使用Arrays.copyOfRange())。

s1和s2是相同的,因为在Java中它们都指的是相同的internedstring。 这是通过Javadevise的。

string是不可变的,但是通过reflection你可以改变String类。 您刚刚将String类重新定义为实时可变的。 你可以重新定义方法是公开还是私人的,或者是静态的。

[免责声明这是一个故意的自以为是的答案式的回答,因为我觉得更“不要在家里做这个孩子”的答案是合理的]

罪是行field.setAccessible(true); 这表示通过允许访问私人领域来违反公共API。 这是一个巨大的安全漏洞,可以通过configuration安全pipe理器来locking。

在这个问题中的现象是实现细节,当不使用危险的代码行通过reflection来违反访问修饰符时,您将永远不会看到这些细节。 显然,两个(通常)不可变的string可以共享相同的字符数组。 子string是否共享相同的数组取决于它是否可以以及开发者是否想要分享它。 通常这些是不可见的实现细节,除非你用这行代码通过头部访问访问修饰符,否则你不应该知道这些细节。

依靠这样的细节是不是一个好主意,这些细节在没有违反使用reflection的访问修饰符的情况下是无法体验的。 该类的所有者仅支持普通的公共API,并且可以在将来自由地进行实现更改。

说了这么一句话,当你拿着枪把你的头压迫你做这么危险的事情的时候,这行代码真的非常有用。 使用后门通常是一个代码气味,你需要升级到更好的库代码,你不必犯罪。 危险的代码行的另一个常见用法是编写一个“voodoo框架”(orm,injection container,…)。 许多人对这样的框架(对于他们来说都是这样)提供了信任,所以我将避免邀请一场火焰战争,除了绝大多数程序员不必去那里之外别的什么都不说。

string是在JVM堆内存的永久区域中创build的。 所以是的,这是真正的不可变的,创build后不能改变。 因为在JVM中,有三种types的堆内存:1.年轻一代2.旧一代3.永久代。

当任何对象被创build时,它将进入年轻一代的堆区和PermGen区域保留为string池。

这里有更多的细节可以去抓取更多的信息: 垃圾收集如何在Java中工作

通过从这里详细阅读原因,您可以清楚地看到“为什么String类被devise为不可变的” 问题

探索String类将使你清楚的看到它是如何devise成不可变的。 点击这里探索string类