奇怪的字符串池行为

73

我有一个奇怪的字符串池行为的问题。

我使用==来比较相等的字符串,以查找它们是否在池中。

public class StringPoolTest {
  public static void main(String[] args) {
    new StringPoolTest().run();
  }

  String giveLiteralString() {
    return "555";
  }

  void run() {
    String s1 = giveLiteralString() + "";
    System.out.println("555" == "555" + "");
    System.out.println(giveLiteralString() == giveLiteralString() + "");
  }
}

输出结果为:

true
false

这对我来说是个惊喜。有人能解释一下吗?我认为这与编译时间有关。但是,为什么向字符串添加""会有任何差异呢?

@MarkoTopolnik 对我来说似乎是一样的。 - johnchen902
5
我知道问题有些不同,但答案总是像这样:“XXX 是编译时常量,而 YYY 不是”。也许我选择了错误的问题。 - johnchen902
1
@johnchen902 我同意,但你把错误的问题标记为重复了 :-) - Thihara
你真的想比较引用还是返回的字符串? - Mare Infinitus
@MareInfinitus 为什么不呢?它比使用 equals 进行比较快得多。当然,您必须确保所有字符串都在池中(例如通过 intern())。 - iozee
4个回答

110
"555" + ""

是一个编译时常量,而

giveLiteralString() + ""

因此前者编译为字符串常量"555",而后者编译为实际方法调用和连接操作,结果生成一个新的字符串实例。


另请参见JLS §3.10.5 (字符串字面值)

在运行时通过连接计算出的字符串是新创建的,并且因此是独立的。


请问您能否提供在方法调用时新字符串实例的引用?有没有JLS链接? - sanbhat
此外,在编译之前/期间运行的代码优化器可能已将"555"+""组合成一个单独的字符串对象"555",但是method()+""在编译后仍然是method()+"" - Korashen
3
请点击我回答中的“编译时常量”,并进行查看。 - Marko Topolnik
附带问题:如果您将 giveLiteralString() 添加 final,会改变什么吗? - Constantino Tsarouhas
@RandyMarsh 不会的:请查看编译时常量允许表达式的列表。 - Marko Topolnik

32

反编译这一行之后

System.out.println("555" == "555" + "");

我得到了这个字节码

    LINENUMBER 8 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ICONST_1
    INVOKEVIRTUAL java/io/PrintStream.println(Z)V
    ...

等同于

  System.out.println(true);

这意味着表达式 "555" == "555" + "" 编译成布尔值 true

对于 giveLiteralString() == giveLiteralString() + "",javac生成了此字节码。

    LINENUMBER 8 L0
    INVOKESTATIC Test1.giveLiteralString()Ljava/lang/String;
    NEW java/lang/StringBuilder
    DUP
    INVOKESTATIC Test1.giveLiteralString()Ljava/lang/String;
    INVOKESTATIC java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String;
    INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
    IF_ACMPNE L1
    ...

相当于

if (giveLiteralString() == new StringBuilder(giveLiteralString()).append("").toString()) {
...

由于我们比较的是两个不同的对象,所以这里将始终生成false。


2
“这将始终产生错误” - 从技术上讲,StringBuilder不需要生成非内部化字符串。只是没有充分的理由去尝试生成一个内部化的字符串。 - Hot Licks
1
从 StringBuilder.toString API - 分配一个新的 String 对象并初始化为当前由该对象表示的字符序列。然后返回此字符串。 - Evgeniy Dorofeev
但是并没有说它不能被实习。 - Hot Licks
1
如果它被interned,就不能保证它是全新的。如果字符串池已经包含该字符串,则intern将返回旧实例。 - Marko Topolnik
2
@HotLicks 我的理解是返回了一个新的字符串对象。这总是新的,而不是来自池中的对象。 - Evgeniy Dorofeev

4
在第二种情况下,编译器本可以认出+ ""是一种无操作的形式,因为""是一个编译时已知长度为零的值。但是,编译器仍然需要检查giveLiteralString的结果是否为空(因为在非优化情况下,空检查会作为+操作的结果发生),所以最简单的方法就是不尝试进行优化。
因此,编译器生成代码来执行连接操作,并创建一个新字符串。

5
编译器必须遵循JLS规范,其中明确指出运行时字符串连接的结果是一个全新的字符串。 - Marko Topolnik

0
编译时拼接 由常量表达式计算的字符串在编译时完成,并被视为常量或字面量,这意味着字符串或表达式的值在编译时已知或已评估,因此编译器可以检查字符串池中的相同值并返回相同的字符串对象引用。
运行时拼接 字符串表达式的值在编译时无法确定或无法评估,但取决于运行时的输入或条件,则编译器将不知道字符串的值,因此始终使用StringBuilder来附加字符串,并始终返回新字符串。 我想这个例子会更好地说明它。
public static void main(String[] args) {
    new StringPoolTest().run();
  }
  String giveLiteralString() {
    return "555";
  }

  void run() {
    System.out.println("555" + 9 == "555" + 9);  
    System.out.println("555"+Integer.valueOf(9) == "555" + Integer.valueOf(9)); 
    System.out.println(giveLiteralString() == giveLiteralString());
    // The result of runtime concatenation is a fresh string.
    System.out.println(giveLiteralString() == giveLiteralString() + "");
  }

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