Java字符串真的是不可变的吗?

Dar*_*tel 391 java string reflection immutability

我们都知道StringJava 中是不可变的,但请检查以下代码:

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  
Run Code Online (Sandbox Code Playgroud)

为什么这个程序运行这样?为什么价值s1s2变化,但不是s3

har*_*ldK 402

String 是不可变的*但这仅表示您无法使用其公共API更改它.

你在这里做的是使用反射来规避正常的API.同样,您可以更改枚举值,更改整数自动装箱等中使用的查找表.

现在,原因s1s2变化值是,它们都引用相同的实习字符串.编译器执行此操作(如其他答案所述).

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

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

*)我最初写道,Strings不是真正不可变的,只是"有效的不可变".这可能会误导当前的实现String,其中value数组确实被标记private final.但是,值得注意的是,没有办法将Java中的数组声明为不可变的,因此必须注意不要将它暴露在类之外,即使使用适当的访问修饰符也是如此.


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

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

  • 在字符串及其子字符串之间共享数组也意味着*every*`String`实例必须携带变量以记住引用数组和长度的偏移量.考虑到字符串的总数以及应用程序中正常字符串和子字符串之间的典型比例,这是一个不要忽略的开销.由于它们必须针对每个字符串操作进行评估,这意味着减慢*每个*字符串操作只是为了**one*操作,一个便宜的子字符串. (9认同)
  • 实际上,`String` interning是JLS的一部分(["字符串文字总是引用类String的同一个实例"](http://docs.oracle.com/javase/specs/jls/se7/html/jls -3.html#JLS-3.10.5)).但我同意,依靠`String`类的实现细节并不是一个好习惯. (7认同)
  • 也许'substring`复制而不是使用现有数组的"section"的原因是,如果我有一个巨大的字符串`s`并从中取出一个名为`t`的小子字符串,我后来放弃了`s `但保持`t`,然后巨大的阵列将保持活着(不是垃圾收集).那么也许每个字符串值更自然地拥有自己的关联数组? (3认同)
  • @Holger - 是的,我的理解是偏移字段在最近的JVM中被删除了.即使它存在,它也不经常使用. (2认同)
  • @supercat:不管你是否有本机代码,在同一个 JVM 中对字符串和子字符串有不同的实现,或者对于 ASCII 字符串有 `byte[]` 字符串,对于其他字符串有 `char[]` 意味着每个操作在操作之前必须检查它是哪种字符串。这阻碍了使用字符串将代码内联到方法中,这是使用调用者的上下文信息进行进一步优化的第一步。这是一个很大的影响。 (2认同)

Zah*_*med 94

在Java中,如果两个字符串原始变量初始化为同一个文字,它会为这两个变量分配相同的引用:

String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true
Run Code Online (Sandbox Code Playgroud)

初始化

这就是比较返回true的原因.创建第三个字符串,使用substring()该字符串创建一个新字符串而不是指向相同的字符串.

子串

使用反射访问字符串时,您将获得实际指针:

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
Run Code Online (Sandbox Code Playgroud)

因此,更改为将更改包含指向它的指针的字符串,但由于s3它创建的新字符串substring()不会更改.

更改

  • @ Zaphod42不对.您也可以在非文字字符串上手动调用`intern`并获得好处. (2认同)

Boh*_*ian 50

你正在使用反射来规避String的不变性 - 它是一种"攻击"形式.

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

有些用例可能会使用这种类型的代码,并且可以"良好编码",例如尽早清除内存中的密码(GC之前).

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


Ank*_*kur 30

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


Hau*_*idt 24

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

你看到的第二个影响就是所有String的变化,而你看起来只会改变s1.Java String文字的某个属性是它们被自动实现,即缓存.具有相同值的两个字符串文字实际上是同一个对象.当您创建一个字符串时new,它将不会自动实现,您将看不到此效果.

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

截至目前的Java和你当前版本的问题没有什么奇怪的行为#substring.

  • 实际上,可见性修饰符*是*(或至少是)用于保护恶意代码的保护 - 但是,您需要设置SecurityManager(System.setSecurityManager())来激活保护.这实际上有多安全是另一个问题...... (2认同)
  • 需要一个upvote,因为你强调访问修饰符**不是为了"保护"代码.这似乎在Java和.NET中被广泛误解.虽然之前的评论与此相矛盾; 我对Java知之甚少,但在.NET中这肯定是正确的.在这两种语言中,用户都不应该认为这会使他们的代码无法防范. (2认同)

Kre*_*ase 11

字符串不变性来自接口的角度.您正在使用反射绕过接口并直接修改String实例的内部.

s1并且s2都被更改,因为它们都被分配给相同的"实习生"String实例.您可以从本文中了解有关字符串相等性和实习的更多信息.您可能会惊讶地发现在示例代码中s1 == s2返回true!


man*_*nta 10

您使用的是哪个版本的Java?从Java 1.7.0_06开始,Oracle改变了String的内部表示形式,尤其是子字符串.

引用Oracle Tunes Java的内部字符串表示:

在新范例中,String偏移量和计数字段已被删除,因此子字符串不再共享底层char []值.

有了这种变化,它可能会在没有反射的情况下发生(???).

  • 如果OP使用较旧的Sun/Oracle JRE,最后一个语句将打印"Java!" (因为他不小心张贴).这只会影响字符串和子字符串之间的值数组的共享.你仍然无法在没有技巧的情况下改变价值,比如反思. (2认同)

Sco*_*ski 7

这里有两个问题:

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

要点1:除ROM外,计算机中没有不可变的内存.如今甚至ROM有时也是可写的.总有一些代码可以写入你的内存地址(无论是内核还是替代你的托管环境的本机代码).所以,在"现实"中,不是它们不是绝对不可改变的.

要点2:这是因为substring可能正在分配一个新的字符串实例,这可能会复制数组.可以以不会复制的方式实现子字符串,但这并不意味着它.需要权衡利弊.

例如,是否应该持有引用以reallyLargeString.substring(reallyLargeString.length - 2)使大量内存保持活动,或者仅保留几个字节?

这取决于子串的实现方式.深拷贝将保持较少的内存,但运行速度会稍慢.浅拷贝会使更多的内存存活,但速度会更快.使用深层副本还可以减少堆碎片,因为字符串对象及其缓冲区可以在一个块中分配,而不是2个单独的堆分配.

无论如何,看起来您的JVM选择使用深层副本进行子串调用.

  • 真实的ROM与塑料封装的照片印刷品一样不变.当晶片(或印刷品)化学显影时,图案永久固定.如果写入它所需的控制信号不能被激励而没有向安装它的电路增加额外的电连接,则电可变存储器(包括RAM芯片*)可以表现为"真正的"ROM.嵌入式设备实际上并不少见,它包括在工厂设置并由备用电池维护的RAM,如果电池出现故障,其内容需要由工厂重新加载. (3认同)
  • @supercat:您的计算机不是那些嵌入式系统之一.:)真正的硬连线ROM在个人电脑中已经不常见了十年或两年; 这些天一切都是EEPROM和闪存.基本上,每个引用内存的用户可见地址都指的是可能可写的内存. (3认同)
  • @supercat我认为你忽略了主题的要点,即存储在RAM中的字符串永远不会真正不可变. (2认同)

Abh*_*hra 5

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

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

所以现在发生的是S1的值被改变(通过使用char []值).因此,s1和s2指向的存储器位置M1的值已经改变.

因此,结果,存储位置M1被修改,这导致s1和s2的值的变化.

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


Mau*_*ark 5

s3 实际上没有改变的原因是因为在 Java 中,当您执行子字符串时,子字符串的值字符数组是在内部复制的(使用 Arrays.copyOfRange())。

s1 和 s2 是相同的,因为在 Java 中它们都指向同一个内部字符串。它是在 Java 中设计的。

  • 这个答案如何在您之前的答案中添加任何内容? (2认同)

And*_*hev 5

要添加到@haraldK的答案 - 这是一个安全黑客,可能会导致应用程序的严重影响.

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

System.out.println("Hello World"); 
Run Code Online (Sandbox Code Playgroud)

将打印:

Hello Java!
Run Code Online (Sandbox Code Playgroud)

当我对这些危险的字符串实施繁重的计算时,我遇到了另一个问题.在计算过程中,有一个错误发生在1000000次中,这使得结果不确定.我能够通过关闭JIT找到问题 - 我总是在关闭JIT的情况下获得相同的结果.我的猜测是因为这个字符串安全黑客破坏了一些JIT优化合同.