首页 文章

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

提问于
浏览
375

我们都知道 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 没有变化?

15 回答

  • 0

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

    s1s2 都因为它们而改变了都被分配给相同的"intern" String实例 . 你可以从this article了解关于字符串相等和实习的更多信息 . 您可能会惊讶地发现在示例代码中, s1 == s2 返回 true

  • 11

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

    s1和s2是相同的,因为在Java中它们都引用相同的实习字符串 . 它是用Java设计的 .

  • 50

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

    您看到的第二个效果是所有 String 都会发生变化,而您似乎只会更改 s1 . Java String文字的某个属性是它们被自动实现,即缓存 . 具有相同值的两个字符串文字实际上是同一个对象 . 使用 new 创建字符串时,它不会自动实现,您将看不到此效果 .

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

    截至当前Java和您当前版本的问题, #substring 没有奇怪的行为 .

  • 392

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

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

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

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

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

  • 5

    字符串在JVM堆内存的永久区域中创建 . 所以是的,它确实是不可变的,在创建后无法更改 . 因为在JVM中,堆内存有三种类型:1 . 年轻代2.老代3.永久代 .

    创建任何对象时,它将进入为字符串池保留的年轻代堆区域和PermGen区域 .

    您可以从以下网站获取更多详细信息:How Garbage Collection works in Java .

  • 1

    [免责声明这是一种刻意的自以为是的答案,因为我觉得更多“不要在家里做这个孩子”答案是有道理的]

    罪行是 field.setAccessible(true); ,它通过允许访问私人领域来违反公共API . 这是一个巨大的安全漏洞,可以通过配置安全管理器来锁定 .

    问题中的现象是实现细节,当您不使用危险的代码行通过反射违反访问修饰符时,您将永远看不到这些细节 . 显然,两个(通常)不可变字符串可以共享相同的char数组 . 子串是否共享相同的数组取决于它是否可以以及开发人员是否想要共享它 . 通常情况下,这些是不可见的实现细节,除非您使用该行代码通过头部拍摄访问修饰符,否则您不必知道 .

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

    说完所有这些代码就非常有用,当你拿着枪时,你的头部强迫你做这些危险的事情 . 使用后门通常是一种代码气味,您需要升级到更好的库代码,而不必犯罪 . 这种危险的代码行的另一个常见用途是编写一个“voodoo框架”(orm,注入容器,......) . 许多人对这样的框架(包括支持和支持这些框架)都持有宗教信仰,所以除了绝大多数程序员不必去那里之外别无他法,我将避免引发一场火焰战争 .

  • 30

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

  • 94

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

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

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

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

    您可以安装 SecurityManager ,以避免恶意代码执行此类操作 . 但请记住,某些库依赖于使用这些反射技巧(通常是ORM工具,AOP库等) .

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


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

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

  • 10

    字符串本质上是不可变的因为没有方法来修改String对象 . 这就是他们引入StringBuilderStringBuffer类的原因

  • 7

    String是不可变的,但通过反射,您可以更改String类 . 您刚刚将String类重新定义为可变的实时 . 如果需要,您可以将方法重新定义为公共或私有或静态方法 .

  • 5

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

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

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

    将打印:

    Hello Java!
    

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

  • 4

    在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);
    

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

  • 2

    这里有两个问题:

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

    • 为什么s3没有改变?

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

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

    例如,如果持有对 reallyLargeString.substring(reallyLargeString.length - 2) 的引用,会导致大量内存保持活动状态,还是只有几个字节?

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

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

  • 1

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

    引自Oracle Tunes Java's Internal String Representation

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

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

  • 24

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

    你可以创建很多这样的例子(例如you can even instantiate a Void object),但这并不意味着String不是"immutable" .

    有些用例可能会使用这种类型的代码,并且"good coding",例如clearing passwords from memory at the earliest possible moment (before GC) .

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

相关问题