Java中字符串的不可变性

237

考虑以下示例。

String str = new String();

str  = "Hello";
System.out.println(str);  //Prints Hello

str = "Help!";
System.out.println(str);  //Prints Help!

现在,在Java中,字符串对象是不可变的。那么,为什么对象str可以被赋值为"Help!"?这岂不是与Java中字符串的不可变性相矛盾吗?请问有人能解释一下不可变性的确切概念吗?

编辑:

好的,我现在明白了,但还有一个后续问题。以下代码怎么说:

String str = "Mississippi"; 
System.out.println(str); // prints Mississippi 

str = str.replace("i", "!"); 
System.out.println(str); // prints M!ss!ss!pp! 

这是否意味着在 replace() 方法之后会再次创建两个对象("Mississippi" 和 "M!ss!ss!pp!"),并且引用 str 指向不同的对象?


1
str仅仅是一个引用,而不是对象本身。 - Ahmed Nabil
1
希望这可以帮到你 https://www.pushkarrajpujari.com/article/strings-in-java/ - Pushkarraj Pujari
26个回答

346

str 不是一个对象,它是指向对象的引用。 "Hello""Help!" 是两个不同的 String 对象。因此,str 指向 一个字符串。你可以改变它所 指向 的内容,但不能改变它所 指向 的对象。

例如,看下面的代码:

String s1 = "Hello";
String s2 = s1;
// s1 and s2 now point at the same string - "Hello"

现在,我们无法对s1进行任何操作,以影响s2的值。它们引用了同一对象-字符串"Hello"-但该对象是不可变的,因此无法更改。

如果我们执行以下操作:

s1 = "Help!";
System.out.println(s2); // still prints "Hello"

在这里,我们看到了改变对象和改变引用之间的区别。 s2 仍然指向与最初设置s1指向的相同对象。 将 s1 设置为 "Help!" 只会改变引用,而其最初所引用的 String 对象保持不变。

如果字符串是可变的,我们可以这样做:

String s1 = "Hello";
String s2 = s1;
s1.setCharAt(1, 'a'); // Fictional method that sets character at a given pos in string
System.out.println(s2); // Prints "Hallo"

针对 OP 的编辑内容进行回答:

如果您查看 String.replace(char,char) 的源代码(也可在 JDK 安装目录中的 src.zip 中找到 -- 一个专业提示是每当你想知道某个东西真正的工作原理时,可以去那里看看),你会发现它所做的操作如下:

  • 如果当前字符串中存在一个或多个 oldChar,则制作一个当前字符串的副本,其中所有的 oldChar 都被替换为 newChar
  • 如果当前字符串中不存在 oldChar,则返回当前字符串。

所以,"Mississippi".replace('i', '!') 创建了一个新的 String 对象。再次强调,以下内容是正确的:

String s1 = "Mississippi";
String s2 = s1;
s1 = s1.replace('i', '!');
System.out.println(s1); // Prints "M!ss!ss!pp!"
System.out.println(s2); // Prints "Mississippi"
System.out.println(s1 == s2); // Prints "false" as s1 and s2 are two different objects

你现在的作业是看看如果你将 s1 = s1.replace('i', '!'); 改成 s1 = s1.replace('Q', '!'); ,上面的代码会做什么 :)


1 实际上,对字符串(和其他不可变对象)进行变异是可能的。这需要反射,并且非常非常危险,除非您真正有兴趣摧毁程序,否则永远不应该使用。


16
为虚构法案的提出点赞,它展示了不可变对象和其他对象之间的差异。 - Zappi
1
感谢gustafc提供正确的示例和清晰的解释...但是您能否回答一下问题中编辑过的部分呢?这将使我的理解更加清晰。 - Light_handle
18
我从未见过这样的回答。讨论了每一个细节。 - Michael 'Maik' Ardan
+1 这里有一个想法,Java中的不可变对象就像按值复制,您可以有两个对字符串的引用,但应将它们视为两个单独的字符串,因为它是不可变的,使用其中一个不会影响另一个。 - Khaled.K
1
Java - 你认为自己知道的越多,实际上你知道的就越少。 - zeeshan
显示剩余5条评论

26

str所引用的对象是可以更改的,但实际的String对象本身无法更改。

包含字符串"Hello""Help!"String对象无法更改其值,因此它们是不可变的。

String对象的不可变性并不意味着指向该对象的引用不能更改。

防止str引用更改的一种方法是将其声明为final

final String STR = "Hello";

现在,试图将另一个String赋值给STR将导致编译错误。


但在这种情况下,String对象是“str”,它首先包含值“Hello”,然后被分配新值“Help!”。“The String objects containing the string "Hello" and "Help!" cannot change their values, hence they are immutable.”的确切含义是什么???如果这是一个愚蠢的问题,请原谅我。但我必须澄清一下... - Light_handle
2
你曾经尝试过用C语言编程吗?只需阅读有关指针的入门指南,你就能完美理解coobird的答案。 - Ryan Fernandes
看,这就是我想要避免的...我知道你是一个很棒的程序员...而我只是在尝试学习Java...所以如果你能正确地回答我的问题,请回答它... - Light_handle
你混淆了引用和对象 - str 不是 "对象",它是指向对象的引用。如果你有 String str = "Hello"; 然后跟着是 String anotherReference = str;,你不会有两个 String 对象,你只有一个对象(字面量 "Hello")和两个对它的引用 (stranotherReference)。 - Nate
我没有编辑权限,但如果有的话,我会编辑coobirds的第一句话为:"str引用可以更改,但实际的String对象本身不能。" - j3App

10

我建议你阅读 Cup Size -- a story about variablesPass-by-Value Please (Cup Size continued)。这将有助于你阅读上述帖子。

你已经阅读过了吗?是的,很好。

String str = new String();

这将创建一个名为 "str" 的新的 "遥控器" 并将其设置为 new String()(或者"")的值。

例如,在内存中,这将创建:

str --- > ""

str  = "Hello";

这将更改远程控制器的 "str",但不会修改原始字符串 ""

例如,在内存中,这将创建:

str -+   ""
     +-> "Hello"

str = "Help!";

这会改变远程控制器中的 "str",但不会修改原始字符串 "" 或当前远程控制器所指向的对象。

例如,在内存中,它将创建:

str -+   ""
     |   "Hello"
     +-> "Help!"

""和"Hello"会被垃圾回收吗? - Prabin Timsina
@PrabinTimsina 这应该是一个新问题。它已经有答案了:https://dev59.com/TWUp5IYBdhLWcg3wXGsZ - Michael Lloyd Lee mlk

9
让我们把它分成几个部分。
String s1 = "hello";

这个语句创建了一个字符串,包含hello,并在内存中占用空间,即在常量字符串池中,并将其分配给引用对象s1

String s2 = s1;

该语句将字符串hello分配给新引用s2
         __________
        |          |
s1 ---->|  hello   |<----- s2
        |__________| 

两个引用都指向同一个字符串,因此输出结果如下。

out.println(s1);    // o/p: hello
out.println(s2);    // o/p: hello

虽然 String不可变的,但是赋值是可能的,所以 s1 现在将引用新值 stack

s1 = "stack";    
         __________
        |          |
s1 ---->|  stack   |
        |__________|

那么,指向 hellos2 对象将保持不变。

         __________
        |          |
s2 ---->|  hello   |
        |__________|

out.println(s1);    // o/p: stack
out.println(s2);    // o/p: hello

由于String是不可变的,Java虚拟机不允许我们通过其方法修改字符串s1。它将在池中创建所有新的String对象,如下所示。

s1.concat(" overflow");

                 ___________________
                |                   |
s1.concat ----> |  stack overflow   |
                |___________________|

out.println(s1);    // o/p: stack
out.println(s2);    // o/p: hello
out.println(s1.concat); // o/p: stack overflow

请注意,如果String是可变的,则输出将会是什么。
out.println(s1);    // o/p: stack overflow

现在你可能会惊讶为什么String有像concat()这样的方法进行修改。接下来的代码片段将澄清你的疑惑。
s1 = s1.concat(" overflow");

在这里,我们将修改后的字符串值分配回s1引用。

         ___________________
        |                   |
s1 ---->|  stack overflow   |
        |___________________|


out.println(s1);    // o/p: stack overflow
out.println(s2);    // o/p: hello

这就是为什么Java决定将String设为final类,否则任何人都可以修改和更改字符串的值。 希望这能有所帮助。

6

str所引用的字符串对象未被更改,你所做的只是让str引用一个新的字符串对象。


5

关于你提出的替换部分问题,可以尝试以下方法:

String str = "Mississippi"; 
System.out.println(str); //Prints Mississippi 

String other = str.replace("i", "!"); 
System.out.println(str); //still prints Mississippi 
System.out.println(other);  // prints M!ss!ss!pp!

5
字符串本身不会改变,只是对它的引用会改变。你把不可变性和“final”字段的概念混淆了。如果一个字段被声明为“final”,一旦被赋值后就不能再次分配。

4
尽管Java试图忽略它,但str只不过是一个指针。这意味着当您首次编写str =“Hello”;时,您创建了一个对象,str指向该对象。当您通过编写str =“Help!”;重新分配str时,将创建一个新对象,并且旧的"Hello"对象会在Java感觉合适时被垃圾收集。

3

String类是不可变的,您无法更改不可变对象的值。 但是在字符串的情况下,如果您更改了字符串的值,那么它将在字符串池中创建新的字符串,然后您的字符串引用该值而不是旧值。因此,通过这种方式,字符串是不可变的。 让我们以您的例子为例,

String str = "Mississippi";  
System.out.println(str); // prints Mississippi 

这将创建一个字符串"Mississippi"并将其添加到字符串池中,现在变量 str 指向了该字符串。

str = str.replace("i", "!");  
System.out.println(str); // prints M!ss!ss!pp! 

但在上述操作之后,将会创建另一个字符串"M!ss!ss!pp!"并将其添加到字符串池中。现在str指向M!ss!ss!pp!,而不是Mississippi。
因此,当您更改字符串对象的值时,它将创建另一个对象并将其添加到字符串池中。
让我们再举一个例子。
String s1 = "Hello"; 
String s2 = "World"; 
String s = s1 + s2;

以上三行代码将把三个字符串对象添加到字符串池中。
1)你好
2)世界
3)你好,世界


3

不变性意味着实例化对象的值不能更改,您永远无法将“Hello”变成“Help!”。

变量str是指向对象的引用,当你给str赋一个新值时,你并没有改变它所引用的对象的值,而是引用了一个不同的对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接