在进一步讨论不可变性之前,让我们先来看一下String
类及其功能,再得出任何结论。
String
的工作原理如下:
String str = "knowledge";
这通常会创建一个包含"knowledge"
的字符串,并分配给str
一个引用。简单吧?现在让我们执行更多的函数:
String s = str
让我们看看下面语句的工作原理:
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"
发生了什么:
- 第一行很简单:创建一个新的
String
"java"
并将s1
引用它。
- 接下来,虚拟机创建了另一个新的
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
,以便没有人可以覆盖其方法的行为。