当进行字符串拼接时,Java字符串池是如何工作的?

8

注意:我不是在比较字符是否相等。因为我知道如何使用String.equals()方法。这个问题是关于String引用的。

当我在备考OCA考试时,开始学习String类以及其不可变性等属性。根据我的了解和阅读,字符串池是Java创建字符串时存储对象的地方,如果新创建一个包含相同值的字符串,它会引用字符串池中的字符串,除非我们使用new关键字,这将创建一个新的引用,即使两个字符串包含相同的值。

例如:

String a = "foo";
String b = "foo";
String c = new String("foo");
boolean ab = (a == b); // This return true
boolean ac = (a == c); // This return false

清楚地讲,这段代码的作用是:第一行代码创建了一个String a = "foo"并将其存储在字符串池中,第二行代码将创建String b并引用到"foo",因为它已经存在于字符串池中。但是第三行代码无论该字符串是否已经存在,都会创建该字符串的新引用。下面是一个小的图形示例: http://cdn.journaldev.com/wp-content/uploads/2012/11/String-Pool-Java1.png 问题出现在以下代码行。当通过连接创建字符串时,Java是否有所不同,或者简单的==比较器具有另一种行为?
例子 A:
String a = "hello" + " world!";
String b = "hello world!";
boolean compare = (a == b); // This return true

示例 B:

a = "hello";
b = "hel" + "lo";
compare = (a == b); // This return true

示例C:

a = "Bye";
a += " bye!";
b = "Bye bye!";
compare = (a == b); // This return false

观看代码运行:(http://ideone.com/fdk6KL)

发生了什么?

编辑

  1. 修复示例B中的错误:b ='hel'+'lo'

  2. 添加有关问题的说明。这不是一个比较问题,因为我知道如何使用String.equals(),问题在于字符串池的引用。


字符串不能使用“==”进行比较,那么条件怎么成立呢? - Omore
4
字符串池不会在每次小的字符串连接操作中添加新内容——想象一下循环中有人打印了一千次 "Some Text" + i + "."。您认为这需要在字符串池中创建成千上万个条目吗? - D M
第一行代码中创建String a = "foo"的说法是不正确的。该行仅创建了对字符串的引用(指向变量),而字符串本身已经存在于字符串池中,在类初始化时被放置在那里。 - Lew Bloch
1
任何创建新的String实例的表达式,例如调用new String(...)或连接操作,都会创建一个新的、非内部化的String实例。 "字符串字面量 - 或者更一般地说,是常量表达式(§15.28)的值的字符串 - 被"内部化"以共享唯一实例,使用方法String.intern。" http://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.10.5 否则必须显式调用intern。阅读精细手册。 - Lew Bloch
3
我想您想了解“编译时常量”的术语。 - chrylis -cautiouslyoptimistic-
显示剩余3条评论
2个回答

13
“当使用字符串拼接时,Java是否会做出不同的事情或者简单的 == 比较器会有不同的行为?”
不会改变其行为,发生的是:
当连接两个字符串字面量`"a" + "b"`时,JVM将这两个值连接起来,然后检查字符串池,然后它意识到该值已经存在于池中,所以只需将此引用简单地赋给字符串。现在更详细地看一下这个简单程序的编译字节码:
public class Test  {    
    public static void main(String... args) {
        String a = "hello world!";
        String b = "hello" + " world!";
        boolean compare = (a == b);
    }
}

简单程序

首先,JVM加载字符串“hello world!”并将其推入字符串池(在这种情况下),然后将其加载到堆栈中(ldc = Load constant)【见图片中的第1点】

然后,它将池中创建的引用赋给局部变量(astore_1)【见图片中的第2点】

请注意,此文字的字符串池中创建的引用是#2【见图片中的第3点】

接下来的操作基本相同:将字符串连接起来,将其推送到运行时常量池(在这种情况下为字符串池),但然后意识到已经存在相同内容的文字,因此使用此引用(#2)并将其分配给一个局部变量(astore_2)。

因此,当您执行(a == b)时,结果为 true ,因为两者都引用字符串池#2,即“hello world!”

不过,您的示例 C 有所不同,因为您使用+=运算符,并在编译为字节码时使用StringBuilder连接字符串,因此这将创建一个新的StringBuilder对象实例,从而指向不同的引用(字符串池vs对象)。


除了你的代码:final String l = "Hello"; final String d = " Motto"; System.out.println((l+d) == "Hello Motto"); 这将打印 true,但如果我们将关键字 final 应用于其中一个字符串,它将打印 false。感谢您的回答。 - xsami
2
由于两个 ldc #2 指令都在 编译 代码中,因此是 编译器 进行了字符串连接和常量折叠。JVM 看到两个指向相同池条目 (#2) 的 ldc 指令。它不必“意识到”这些常量是相同的,这是显而易见的。在运行时,没有字符串连接发生。JVM 甚至不知道该常量的第二次使用在编译时已经进行了字符串连接。 - Holger
1
有趣的阅读,关于同一主题 - https://www.quora.com/Java-When-we-concatenate-two-strings-using-the+-operator-will-the-resulting-string-be-stored-in-the-string-literal-pool-or-not?share=1 - Pavan Kumar
2
关于“示例C”的答案是不正确的。首先:+=运算符在编译为字节码时不使用StringBuilder来连接字符串,而是使用StringConcatFactory.makeConcatWithConstants。其次:它返回false,因为使用+=运算符时,新创建的字符串不会添加到字符串池中。使用文字字符串表示法(双引号括起来的文本)和标记为final变量的字符串会被添加到字符串池中。连接的字符串将作为堆上的新对象创建。与字符串池中的字符串进行比较时,它们具有不同的引用。 - Miko
好的,感谢评论,那么我理解了,当串联在声明的同一行时,编译器将代码转换为一个字面量,以简化代码。 对于我们来说,在同一行中有几个变量,但对于编译器来说,它只是一个(最终替换任何变量,它是一个文字),因此比较是true; 另一方面,当我们在不同的行中进行串联时,情况并非如此,因为它不是文字,所以这些变量将不会位于字符串池中。 - J. Abel

3
String a = "ef";
String b = "cd" + a;
        
System.out.println("cdef"==b); // false
        
String c = "cd" + "ef";
        
System.out.println("cdef"==c); // true

当在一个String对象上调用intern()方法时,它会尝试在池中查找具有相同字符序列的字符串。

如果字符串池中已经有相同值的字符串,则返回该字符串在池中的引用,否则将该字符串添加到池中并返回引用。

仅当表达式是常量表达式时,字符串连接才会对字符串进行整理。

"cd" + a // is not a Constant Expression

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