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

时间:2014-01-06 07:26:52

标签: java string reflection immutability

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

16 个答案:

答案 0 :(得分:398)

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

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

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

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

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

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


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

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

答案 1 :(得分:94)

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

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

initialization

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

  

sub string

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

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

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

  

change

答案 2 :(得分:50)

你正在使用反射来规避String的不变性 - 这是一种“攻击”。

你可以创建很多像这样的例子(例如you can even instantiate a Void object),但这并不意味着String不是“不可变的”。

在某些用例中,这类代码可能会对您有利,并且是“良好的编码”,例如clearing passwords from memory at the earliest possible moment (before GC)

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

答案 3 :(得分:30)

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

答案 4 :(得分:24)

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

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

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

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

答案 5 :(得分:11)

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

s1s2都被更改,因为它们都被分配到相同的“实习生”String实例。您可以从this article了解有关字符串相等和实习的更多信息。您可能会惊讶地发现,在示例代码中,s1 == s2会返回true

答案 6 :(得分:10)

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

引自Oracle Tunes Java's Internal String Representation

  

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

有了这个改变,它可能会在没有反射的情况下发生(???)。

答案 7 :(得分:7)

这里确实有两个问题:

  1. 字符串真的是不可变的吗?
  2. 为什么s3没有改变?
  3. 要点1:除ROM外,计算机中没有不可变的内存。如今甚至ROM有时也是可写的。总有一些代码可以写入你的内存地址(无论是内核还是替代你的托管环境的本机代码)。所以,在“现实”中,没有他们不是绝对不可变。

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

    例如,是否应该保持对reallyLargeString.substring(reallyLargeString.length - 2)的引用会导致大量内存保持活动状态,或者仅保留几个字节?

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

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

答案 8 :(得分:5)

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

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

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

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

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

答案 9 :(得分:5)

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

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

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

将打印:

Hello Java!

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

答案 10 :(得分:4)

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

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

答案 11 :(得分:2)

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

答案 12 :(得分:1)

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

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

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

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

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

答案 13 :(得分:1)

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

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

您可以访问以下详细信息并获取更多信息: How Garbage Collection works in Java

答案 14 :(得分:0)

String本质上是不可变的,因为没有方法可以修改String对象。 这就是他们引入StringBuilderStringBuffer

的原因

答案 15 :(得分:0)

这是所有内容的快速指南


        // 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