奇怪的字符串池行为


72

我有一些奇怪的字符串池行为的问题。我正在使用==比较相等的字符串来确定它们是否在池中。

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

这对我来说是一个很大的惊喜。有人可以解释一下吗?我认为这是在编译时发生的。但是,为什么添加""到String根本没有任何区别呢?


@MarkoTopolnik在我看来也一样。
johnchen902

5
@MarkoTopolnik我知道这个问题略有不同。但是答案总是像“ XXX是编译时常量,而YYY不是”。不过,也许我选择了一个错误的问题。
johnchen902

1
@ johnchen902我同意,但是您将错误的问题重复了:-)
Thihara

您是否真的要比较引用或返回的字符串?
无限母马2013年

@MareInfinitus是的,为什么不呢?它比使用等于进行比较要快得多。当然,您必须确保所有字符串都在池中(例如,通过intern())。
iozee

Answers:


110
"555" + ""

是一个编译时常量,而

giveLiteralString() + ""

不是。因此,前者仅编译为字符串常量“ 555”,而后者则编译为实际的方法调用和连接,从而生成一个新的String实例。


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

在运行时通过串联计算的字符串是新创建的,因此是不同的。


在方法调用的情况下,能否为新鲜的字符串实例提供引用?任何JLS链接?
sanbhat

同样,在编译之前/期间运行的代码优化器可能已经合并"555"+""为单个字符串对象,"555" 而在编译之后method()+""仍会存在method()+""
Korashen

3
@sanbhat在我的答案中单击“编译时常数”。
Marko Topolnik

附带的问题:如果要添加finalgiveLiteralString(),它会改变什么吗?
Constantino Tsarouhas

@RandyMarsh不会:查看编译时常量的允许表达式列表。
Marko Topolnik

31

反编译此行后

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" + ""编译为boolean 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来生成非中间字符串。只是没有充分的理由试图生产一个实习生。
热门点击2013年

1
从StringBuilder.toString API-分配并初始化一个新的String对象,以包含此对象当前表示的字符序列。然后返回此字符串。
Evgeniy Dorofeev

但是没有人说它不能被拘留。
热门点击2013年

1
@HotLicks如果它是被拘禁的,则不可能保证它实际上是新的。如果字符串池已经包含该字符串,则将由返回旧实例intern
Marko Topolnik

2
@HotLicks我在返回新的String对象时将其读取。这总是新的,不是池中的对象
Evgeniy Dorofeev

4

在第二种情况下,编译器可以识别出+ ""是无操作的,因为""已知编译时值为零长度。但是仍然需要编译器检查来自的结果giveLiteralString是否为null(因为+在未优化的情况下,由于操作的结果会发生null检查),因此最简单的是不尝试优化。

结果,编译器生成执行连接的代码,并创建一个新字符串。


5
编译器必须遵守JLS,该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() + "");
  }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.