字符串是不可变的。这句话的意思是什么?

229

我写了以下关于不可变字符串的代码。

public class ImmutableStrings {

    public static void main(String[] args) {
        testmethod();
    }

    private static void testmethod() {
        String a = "a";
        System.out.println("a 1-->" + a);
        a = "ty";
        System.out.println("a 2-->" + a);
    }
}

输出:

a 1-->a  
a 2-->ty

这里改变了变量a的值(虽然很多人说不可变对象的内容不能被改变)。但是,当我们说“String是不可变的”时,究竟是什么意思呢?您能否为我澄清一下这个话题?
来源:https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

7
在内存方面,字符串是不可变的。每当您创建字符串或分配新字符串/更改其值时,它都会创建新对象。因此,在使用字符串时建议小心。Charbuffer 可能更好。 - user982733
2
请查看此链接以获得更好的解释:https://dev59.com/f3VD5IYBdhLWcg3wGHeu。 - user982733
2
字符串是不可变的(一旦创建就不能更改)对象。作为字符串创建的对象存储在常量字符串池中。 Java中的每个不可变对象都是线程安全的,这意味着字符串也是线程安全的。字符串不能同时被两个线程使用。 一旦分配了字符串,就无法更改。 - user2989087
19个回答

472

在进一步讨论不可变性之前,让我们先来看一下String类及其功能,再得出任何结论。

String的工作原理如下:

String str = "knowledge";

这通常会创建一个包含"knowledge"的字符串,并分配给str一个引用。简单吧?现在让我们执行更多的函数:

 String s = str;     // assigns a new reference to the same string "knowledge"

让我们看看下面语句的工作原理:

  str = str.concat(" base");

这将在str后面添加一个字符串" base",但是等等,这怎么可能呢,因为String对象是不可变的?令人惊讶的是,它确实可以。

当执行上述语句时,虚拟机获取String str的值,即"knowledge"并附加" base",从而给我们一个值"knowledge base"。由于String是不可变的,虚拟机无法将此值赋给str,因此它创建了一个新的String对象,赋予其一个值"knowledge base",并赋予该对象一个引用str

这里需要注意的重要一点是,虽然String对象是不可变的,但它的引用变量不是。因此,在上面的示例中,引用被指向一个新形成的String对象。

在上面的示例中,我们有两个String对象:第一个是我们创建的值为"knowledge"的对象,由s指向,第二个是"knowledge base"的对象,由str指向。但是,从技术上讲,我们有三个String对象,第三个是concat语句中的字面值"base"

String和内存使用的重要事实

如果我们没有对"knowledge"有另一个引用s,我们将失去那个String。然而,它仍会存在,但由于没有引用而被视为已丢失。请看下面的另一个例子。

String s1 = "java";
s1.concat(" rules");
System.out.println("s1 refers to "+s1);  // Yes, s1 still refers to "java"

发生了什么:

  1. 第一行很简单:创建一个新的String "java"并将s1引用它。
  2. 接下来,虚拟机创建了另一个新的String "java rules",但没有任何东西引用它。因此,第二个String立即丢失。我们无法访问它。

引用变量s1仍然引用原始的String "java"

几乎每个用于修改String对象的方法都会创建一个新的String对象。那么这些String对象去哪了呢?嗯,它们存在于内存中,而任何编程语言的关键目标之一就是有效地利用内存。

随着应用程序的增长,字符串字面量占据大量的内存区域非常普遍,甚至可能导致冗余。因此,为了使Java更加高效,JVM设置了一个特殊的内存区域,称为“字符串常量池”。

当编译器看到一个String字面量时,它会在池中查找String。如果找到匹配项,则将新字面量的引用指向现有String,并且不会创建新的String对象。现有的String只是多了一个引用。这就涉及到使String对象不可变的要点:

String常量池中,一个String对象很可能有一个或多个引用。如果有多个引用指向同一个String而且没有意识到,那么如果其中一个引用修改了该String值,那就会很糟糕。这就是为什么String对象是不可变的原因。

好吧,现在你可能会说,如果有人覆盖了String类的功能怎么办?这就是为什么String类被标记为final,以便没有人可以覆盖其方法的行为。


24
好的解释!"If several references point to same String without even knowing it, it would be bad if one of the references modified that String value. That's why String objects are immutable." 如果有几个引用指向同一个字符串而不知道它,如果其中一个引用修改了该字符串的值,则会产生问题。这就是为什么String对象是不可变的原因。"what if someone overrides the functionality of String class? That's the reason that the String class is marked final so that nobody can override the behavior of its methods."如果有人重写了String类的功能怎么办?这就是String类被标记为final的原因,以便没有人可以覆盖其方法的行为。 - Gab是好人
16
我认为这个答案甚至比被采纳的答案更好! - Iching Chang
1
我喜欢这个答案。谢谢你。 - For Testing
1
优秀的解释。 - Siva R
1
那最后一段真的很有帮助。谢谢。 - Zach Smith
显示剩余10条评论

176

String是不可变的,这意味着您不能更改对象本身,但可以更改对对象的引用。

当您执行a = "ty"时,实际上是将a的引用更改为由String字面值"ty"创建的新对象。

更改一个对象意味着使用其方法来更改其中一个字段(或者字段是公共的而不是final的,因此可以从外部更新而不需要通过方法访问它们),例如:

Foo x = new Foo("the field");
x.setField("a new field");
System.out.println(x.getField()); // prints "a new field"

在一个不可变类(被声明为final,以防止通过继承进行修改)中(它的方法不能修改它的字段,并且字段始终是私有的,并推荐使用final),例如String,在其中您无法更改当前的String,但可以返回一个新的String,即:

String s = "some text";
s.substring(0,4);
System.out.println(s); // still printing "some text"
String a = s.substring(0,4);
System.out.println(a); // prints "some"

你提供的关于String的示例非常有用。谢谢。 - Neo

19

你正在改变a所指向的对象。试试这样做:

String a="a";
System.out.println("a 1-->"+a);
String b=a;
a="ty";
System.out.println("a 2-->"+a);
System.out.println("b  -->"+b);
你会看到ab引用的对象没有改变。如果你想防止代码更改a引用的对象,请尝试:
final String a="a";

String a = new String("a"); 现在,我已经创建了对象'a'。如果我尝试修改这个对象'a'中的值,那是不可能的吗? - Anuj Balan
1
没错。在Java中,String a = "a";基本上意味着与String a = new String("a");相同(除了字符串插入)。 - Greg Hewgill
最后一个疑问。现在我明白了我正在改变引用,这是被允许的。当我们说 String 是不可变的时,什么是不被允许的?改变对象的内容!!! 你能举个例子吗?我的意思是不能用 String 做什么? - Anuj Balan
3
String类是不可变的,因为它没有任何可以改变其内容的方法。相比之下,StringBuilder具有.append().delete()等方法,并且不是不可变的。 - Greg Hewgill
@Ajj,请看我为你提供的示例答案 :) - Eng.Fouad
我相信你可以使用反射改变String的值,从而让它们不是完全不可变的。但我并不完全确定。 - user982733

5

一个字符串是一个包含一系列UTF-16代码单元、一个指向该数组的int偏移量和一个int长度的char[]

例如。

String s

它为字符串引用创建空间。将副本分配给引用,但不修改这些引用所指向的对象。

您还应该注意

new String(s)

这段代码并没有实现任何有用的功能。它仅仅创建了另一个实例,该实例由与s相同的数组、偏移量和长度支持。很少有理由这样做,因此大多数Java程序员认为这是一种不良实践。

Java双引号字符串,例如"my string",实际上是对内部化String实例的引用,因此无论在您的代码中出现多少次,"bar"都是对同一String实例的引用。


“hello”创建了一个被池化的实例,而new String(...)则创建了一个非池化实例。尝试System.out.println(("hello" == "hello") + "," + (new String("hello") == "hello") + "," + (new String("hello") == new String("hello")));,你应该会看到true,false,false


4

Immutable意味着您无法更改相同引用的值。每次需要创建新引用时,都需要使用新的内存位置。 例如:

String str="abc";
str="bcd";

在上面的代码中,内存中有两个块用于存储值。第一个用于值“abc”,第二个用于“bcd”。第二个值没有替换第一个值。

这被称为不可变性。


1
这并没有回答 OP 的问题,因为不可变性与 String 值的修改有关。但在这里,您只是展示了两个不同的字符串字面量并将其分配给相同的引用。 - SacJn

3
在你的例子中,变量a只是一个字符串对象实例的引用。当你执行a = "ty"时,你实际上并没有改变这个字符串对象,而是将引用指向了完全不同的字符串类实例。

3

请点击这里查看。

class ImmutableStrings {

    public static void main(String[] args) {
        testmethod();
    }

    private static void testmethod() {
    String a="a";
    System.out.println("a 1-->"+a);
    System.out.println("a 1 address-->"+a.hashCode());

    a = "ty";
    System.out.println("a 2-->"+a);

       System.out.println("a 2 address-->"+a.hashCode());
    }
}

输出:

a 1-->a
a 1 address-->97
a 2-->ty
a 2 address-->3717

这表明,无论何时修改不可变字符串对象a的内容,都会创建一个新的对象。也就是说,你不能更改不可变对象的内容。这就是为什么两个对象的地址不同的原因。

a = "ty"; 是堆中的新对象。对于 String a ="a"; 会发生什么? - Anshul Tyagi

2

Java中的String是不可变的,String会以对象形式存储值。因此,如果您赋值String a="a";,它将创建一个对象并将值存储在其中,如果再次赋值a="ty",那么它将创建另一个对象并将值存储在其中。如果您想要更清楚地了解,请检查StringhashCode


2
你实际上获取的是一个新字符串的引用,因为它是不可变的,所以字符串本身并没有被改变。这一点很重要。
参见:
不可变对象 - 维基百科

2
一个不可变对象是指其状态在创建后无法修改的对象。
因此,a = "ABC"是一个不可变对象。"a"保存了对该对象的引用。 而a = "DEF"是另一个不可变对象,现在"a"保存了对它的引用。
一旦您分配了一个字符串对象,该对象就不能在内存中更改。
总之,您所做的是将"a"的引用更改为一个新的字符串对象。

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