我在不可变字符串上写了下面的代码。

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的值被改变了(尽管许多人说不可变对象的内容不能被改变)。但是说字符串是不可变的到底是什么意思呢?你能给我解释一下这个话题吗?

来源:https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html


当前回答

字符串是不可变的,这意味着,字符串对象的内容不能被改变,一旦它被创建。如果你想修改内容,那么你可以去StringBuffer/StringBuilder而不是String。StringBuffer和StringBuilder是可变类。

其他回答

在进一步讨论不可变性之前,让我们在得出任何结论之前稍微了解一下String类及其功能。

这是String的工作原理:

String str = "knowledge";

这,像往常一样,创建一个包含“知识”的字符串,并为其分配一个引用str。简单吗?让我们执行更多的函数:

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

让我们看看下面的语句是如何工作的:

  str = str.concat(" base");

这将追加一个字符串“base”到str。但等一下,这是怎么可能的,因为字符串对象是不可变的?出乎你的意料,它是。

当执行上述语句时,虚拟机接受String str的值,即。“knowledge”并附加“base”,赋予我们“knowledge base”这个值。现在,由于字符串是不可变的,VM不能将这个值分配给str,所以它创建了一个新的String对象,给它一个值“知识库”,并给它一个引用str。

这里需要注意的一点是,虽然String对象是不可变的,但它的引用变量不是。这就是为什么在上面的例子中,引用被用来引用一个新形成的String对象。

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

关于字符串和内存使用的重要事实

如果我们对“知识”没有另一种指称会怎样?我们会失去那个字符串。然而,它仍然存在,但由于没有参考资料,将被视为丢失。 下面再看一个例子

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

发生了什么:

第一行非常简单:创建一个新的String "java",并将s1引用给它。 接下来,虚拟机创建另一个新的字符串“java rules”,但是什么都没有 指它。因此,第二个字符串立即丢失。我们够不到 它。

引用变量s1仍然引用原始字符串“java”。

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

随着应用程序的增长,字符串字面值占用大量内存是很常见的,这甚至会导致冗余。因此,为了使Java更高效,JVM留出了一个特殊的内存区域,称为“字符串常量池”。

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

在String常量池中,一个String对象可能有一个或多个引用。如果几个引用指向同一个String而不知道它,如果其中一个引用修改了该String值,那就不好了。这就是为什么String对象是不可变的。

现在你可以说,如果有人重写了String类的功能呢?这就是String类被标记为final的原因,这样任何人都不能重写其方法的行为。

希望下面的代码能澄清你的疑问:

public static void testString() {
    String str = "Hello";
    System.out.println("Before String Concat: "+str);
    str.concat("World");
    System.out.println("After String Concat: "+str);
    StringBuffer sb = new StringBuffer("Hello");
    System.out.println("Before StringBuffer Append: "+sb);
    sb.append("World");
    System.out.println("After StringBuffer Append: "+sb);
}

字符串Concat:你好 字符串后Concat:你好 在StringBuffer追加之前:Hello StringBuffer后追加:HelloWorld

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

为例。

String s

它为字符串引用创建空间。赋值复制周围的引用,但不修改这些引用所引用的对象。

你也应该意识到

new String(s)

并没有做任何有用的事情。它只是创建了另一个实例,支持的数组、偏移量和长度与s相同。很少有理由这样做,因此大多数Java程序员认为这是一种糟糕的做法。

Java双引号字符串,如“my string”,实际上是对内部string实例的引用,因此“bar”是对同一个string实例的引用,而不管它在代码中出现了多少次。


“hello”创建了一个被池化的实例,而新的String(…)创建了一个非池化的实例。尝试system . out。println((“hello”= =“你好 ") + "," + ( 新的字符串“hello”= =“你好 ") + "," + ( 新的字符串(“hello”)= =新的字符串(“hello”)));你应该看到真,假,假

你不是在改变赋值语句中的对象,而是用另一个不可变对象替换了一个不可变对象。Object String("a")不会变成String("ty"),它会被丢弃,对ty的引用会被写入a中。

相反,StringBuffer表示一个可变对象。你可以这样做:

StringBuffer b = new StringBuffer("Hello");
System.out.writeln(b);
b.append(", world!");
System.out.writeln(b);

在这里,您没有重新分配b:它仍然指向相同的对象,但该对象的内容已经更改。

字符串是不可变的,这意味着你不能改变对象本身,但是你可以改变对对象的引用。

当你执行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"