我们都知道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的值不变?


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


您正在使用反射来规避String的不可变性——这是一种“攻击”形式。

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

在一些用例中,这种类型的代码可能会被用于您的优势,并且是“良好的代码”,例如在尽可能早的时候(在GC之前)从内存中清除密码。

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


字符串是不可变的*,但这只意味着你不能使用它的公共API更改它。

您在这里所做的是绕过常规API,使用反射。同样地,你可以改变枚举的值,改变Integer自动装箱中使用的查找表等等。

s1和s2改变值的原因是,它们都指向同一个被合并的字符串。编译器执行此操作(如其他答案所述)。

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

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

*)我最初写的字符串不是真正的不可变,只是“有效的不可变”。在String的当前实现中,这可能会引起误解,其中值数组确实被标记为private final。不过,仍然值得注意的是,在Java中没有办法将数组声明为不可变的,因此必须注意不要在类之外公开它,即使使用适当的访问修饰符。


由于这个话题似乎非常受欢迎,这里有一些建议的进一步阅读:Heinz Kabutz在JavaZone 2009上的Reflection Madness演讲,它涵盖了OP中的许多问题,以及其他的反思……嗯…疯狂。

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


字符串不可变性是从接口的角度来看的。您使用反射绕过接口,直接修改String实例的内部结构。

s1和s2都被改变了,因为它们都被分配给了同一个“intern”String实例。您可以从这篇关于字符串相等和实习的文章中找到更多关于这部分的内容。您可能会惊讶地发现,在示例代码中,s1 == s2返回true!


可见性修饰符和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没有奇怪的行为。


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

因此,更改this将更改持有指向它的指针的字符串,但由于substring()使用新字符串创建了s3,因此它不会更改。


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

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

在新的范例中,String offset和count字段已被删除,因此子字符串不再共享底层char[]值。

有了这个变化,它可能在没有反思的情况下发生(??)


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

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

所以现在所发生的是S1的值被改变了(通过使用char[]值)。因此,由s1和s2指向的内存位置M1的值被改变了。

因此,内存位置M1被修改,导致s1和s2的值发生变化。

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


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


这里有两个问题:

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

第一点:除了ROM,在你的计算机中没有不可变的内存。现在甚至ROM有时也是可写的。总有一些代码(无论是绕过托管环境的内核代码还是本机代码)可以写入内存地址。所以,在“现实”中,它们不是绝对不变的。

对于第2点:这是因为substring可能分配了一个新的字符串实例,这可能是复制数组。substring有可能以不复制的方式实现,但这并不意味着它会这样做。这涉及到权衡。

例如,应该持有对reallyLargeString.substring(reallyLargeString. substring)的引用。长度- 2)导致大量的内存保持活跃,还是只有几个字节?

这取决于substring如何实现。深度复制将保留较少的活动内存,但运行速度会稍微慢一些。浅拷贝可以保留更多的内存,但速度更快。使用深度拷贝还可以减少堆碎片,因为字符串对象及其缓冲区可以分配在一个块中,而不是两个单独的堆分配。

在任何情况下,看起来您的JVM都选择对子字符串调用使用深度拷贝。


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

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


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

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

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

将打印:

Hello Java!

当我在这样危险的字符串上实现繁重的计算时,我遇到了另一个问题。在计算过程中出现了一个bug,大约发生了100万分之一的概率,这使得结果不确定。我能够通过关闭JIT来发现问题——我总是得到相同的结果,关闭JIT。我猜原因是这个字符串安全黑客破坏了一些JIT优化契约。


[免责声明,这是一个故意固执己见的回答风格,因为我觉得一个更“不要在家里这样做,孩子们”的回答是有保证的]

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.


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

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

以下是更多细节,你可以从中获取更多信息: 垃圾收集在Java中的工作原理。


字符串本质上是不可变的,因为没有方法来修改字符串对象。 这就是他们引入StringBuilder和StringBuffer类的原因


这是一个快速指南的一切


        // Character array
        char[] chr = {'O', 'K', '!'};

        // this is String class
        String str1 = new String(chr);
        
        // this is concat
        str1 = str1.concat("another string's ");
        
        // this is format
        System.out.println(String.format(str1 + " %s ", "string"));
        
        // this is equals
        System.out.println(str1.equals("another string"));

        //this is split
        for(String s: str1.split(" ")){
            System.out.println(s);
        }

        // this is length
        System.out.println(str1.length());

        //gives an score of the total change in the length
        System.out.println(str1.compareTo("OK!another string string's"));

        // trim
        System.out.println(str1.trim());

        // intern
        System.out.println(str1.intern());

        // character at
        System.out.println(str1.charAt(5));

        // substring
        System.out.println(str1.substring(5, 12));

        // to uppercase
        System.out.println(str1.toUpperCase());

        // to lowerCase
        System.out.println(str1.toLowerCase());

        // replace
        System.out.println(str1.replace("another", "hello"));

       //   output

        // OK!another string's  string 
        // false
        // OK!another
        // string's
        // 20
        // 7
        // OK!another string's
        // OK!another string's 
        // o
        // other s
        // OK!ANOTHER STRING'S 
        // ok!another string's 
        // OK!hello string's