在Java中比较使用final声明的字符串时应该使用“==”吗?

224

我对Java中的字符串有个简单的问题。下面这段简单的代码将两个字符串连接起来,然后使用==运算符进行比较。

String str1="str";
String str2="ing";
String concat=str1+str2;

System.out.println(concat=="string");

比较表达式concat=="string"返回false,这是显而易见的(我理解equals()==之间的差异)。


当这两个字符串声明为final时,

final String str1="str";
final String str2="ing";
String concat=str1+str2;

System.out.println(concat=="string");

在这种情况下,比较表达式concat=="string"返回true。为什么final会有所不同?它是否与内部池有关或者我被误导了?


23
我一直觉得使用"equals"作为默认的检查相等内容的方法有些愚蠢,应该使用"=="来进行判断,只需要使用"referenceEquals"或类似方法来检查指针是否相同。 - Davio
26
这并不是与“如何在Java中比较字符串?”有任何重复之处。提问者已经理解了在字符串上下文中equals()==的区别,并且正在询问一个更有意义的问题。 - arshajii
@Davio 但是如果类不是String,那该怎么办呢?我认为使用equals进行内容比较非常合理,而使用==进行身份比较。我们可以重写equals方法来告诉程序什么情况下我们认为两个对象相等。如果使用==进行内容比较,我们就无法重写它来定义“相等内容”的含义。而且,仅针对Stringequals==的含义颠倒也是很愚蠢的。此外,无论如何,我也看不出使用==进行内容比较有任何优势。 - SantiBailors
@SantiBailors,你说得对,这就是Java的工作方式,我也用过C#,在那里“==”被重载为内容相等。使用“==”的额外好处是它是空安全的:(null ==“something”)返回false。如果你使用equals来比较两个对象,你必须意识到任何一个对象都可能为空,否则你会冒NullPointerException的风险。 - Davio
6个回答

235
当您将一个(不可变的)String变量声明为final并使用编译时常量表达式进行初始化时,它也成为编译时常量表达式,并且其值在编译器用于它的地方被内联。因此,在您的第二个代码示例中,在内联值之后,编译器将字符串连接翻译为:
String concat = "str" + "ing";  // which then becomes `String concat = "string";`
"string" 进行比较,将返回 true,因为字符串常量被 interned

来自JLS §4.12.4 - final 变量

一个原始类型或 String 类型的变量,如果它是 final 的并且用编译时常量表达式(§15.28)初始化,那么该变量就被称为 constant variable

同样来自JLS §15.28 - Constant Expression

类型为 String 的编译时常量表达式总是被 "interned",以共享唯一实例,使用方法String#intern()


在您的第一个代码示例中不是这种情况,因为这些 String 变量不是 final。因此,它们不是编译时常量表达式。那里的连接操作将延迟到运行时,从而导致创建新的 String 对象。您可以通过比较两个代码片段的字节码来验证这一点。

第一个代码示例(非 final 版本)编译为以下字节码:

  Code:
   0:   ldc     #2; //String str
   2:   astore_1
   3:   ldc     #3; //String ing
   5:   astore_2
   6:   new     #4; //class java/lang/StringBuilder
   9:   dup
   10:  invokespecial   #5; //Method java/lang/StringBuilder."<init>":()V
   13:  aload_1
   14:  invokevirtual   #6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   17:  aload_2
   18:  invokevirtual   #6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   21:  invokevirtual   #7; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   24:  astore_3
   25:  getstatic       #8; //Field java/lang/System.out:Ljava/io/PrintStream;
   28:  aload_3
   29:  ldc     #9; //String string
   31:  if_acmpne       38
   34:  iconst_1
   35:  goto    39
   38:  iconst_0
   39:  invokevirtual   #10; //Method java/io/PrintStream.println:(Z)V
   42:  return

显然,它将string存储在两个单独的变量中,并使用StringBuilder执行连接操作。

而你的第二个代码示例final版本)看起来像这样:

  Code:
   0:   ldc     #2; //String string
   2:   astore_3
   3:   getstatic       #3; //Field java/lang/System.out:Ljava/io/PrintStream;
   6:   aload_3
   7:   ldc     #2; //String string
   9:   if_acmpne       16
   12:  iconst_1
   13:  goto    17
   16:  iconst_0
   17:  invokevirtual   #4; //Method java/io/PrintStream.println:(Z)V
   20:  return

因此它直接将最终变量内联到编译时创建的Stringstring中,由步骤0中的ldc操作加载。然后第二个字符串字面值由步骤7中的ldc操作加载。它不涉及在运行时创建任何新的String对象。这个String在编译时已知,并被interned。


2
有什么阻止其他Java编译器实现不对final String进行内部化的吗? - Alvin
13
JLS要求编译时常量字符串表达式必须被intern。任何符合规范的实现都必须执行相同的操作。 - Tavian Barnes
相反,JLS是否规定编译器不得优化第一个非最终版本中的连接?编译器是否被禁止生成会使比较评估为“true”的代码? - phant0m
1
@phant0m根据规范的当前措辞,“除非表达式是常量表达式(§15.28),否则将新创建String对象(§12.5)。”如果在non-final版本中应用优化,则不允许,“新创建”的字符串必须具有不同的对象标识。我不知道这是否是有意的。毕竟,当前的编译策略是委托给运行时工具来执行,该工具没有记录此类限制。 - Holger

31
根据我的研究,Java中所有的final String都是被内部化的。来自其中一篇博客文章的内容:
所以,如果你需要使用==或!=比较两个字符串,请确保在进行比较之前调用String.intern()方法。否则,始终优先使用String.equals(String)进行字符串比较。
这意味着如果你调用了String.intern(),你可以使用==运算符比较两个字符串。但是,在Java中final String已经被内部化了,因此String.intern()不是必需的。
你可以在使用==运算符的字符串比较String.intern()方法的Javadoc中找到更多信息。
此外,还可以参考Stackoverflow上的帖子获取更多信息。

3
"intern() "方法返回的字符串不会被垃圾回收,而是存储在"permgen space"中,该空间较小。因此,如果没有正确使用,可能会导致"out of memory error"等问题。请注意避免此类情况。 - Ajeesh
1
@Ajeesh - 内部字符串可以被垃圾回收。即使是由常量表达式生成的内部字符串,在某些情况下也可以被垃圾回收。 - Stephen C

21
如果您查看此方法
public void noFinal() {
    String str1 = "str";
    String str2 = "ing";
    String concat = str1 + str2;

    System.out.println(concat == "string");
}

public void withFinal() {
    final String str1 = "str";
    final String str2 = "ing";
    String concat = str1 + str2;

    System.out.println(concat == "string");
}

使用javap -c ClassWithTheseMethods反编译,你将看到版本信息。

  public void noFinal();
    Code:
       0: ldc           #15                 // String str
       2: astore_1      
       3: ldc           #17                 // String ing
       5: astore_2      
       6: new           #19                 // class java/lang/StringBuilder
       9: dup           
      10: aload_1       
      11: invokestatic  #21                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
      14: invokespecial #27                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
      17: aload_2       
      18: invokevirtual #30                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: invokevirtual #34                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      ...

并且

  public void withFinal();
    Code:
       0: ldc           #15                 // String str
       2: astore_1      
       3: ldc           #17                 // String ing
       5: astore_2      
       6: ldc           #44                 // String string
       8: astore_3      
       ...

因此,如果字符串不是final,则编译器将使用StringBuilder来连接str1str2,因此

String concat=str1+str2;

将被编译为

String concat = new StringBuilder(str1).append(str2).toString();

这意味着concat将在运行时创建,因此不会来自字符串池。


另外,如果字符串是final类型,则编译器可以假定它们永远不会更改,因此它可以安全地连接其值而无需使用StringBuilder

String concat = str1 + str2;

可以更改为

String concat = "str" + "ing";

并连接成

String concat = "string";

这意味着 concate 会成为字符串字面值,存储在字符串池中,并与相同的来自该池中字符串字面值进行比较,以供if语句使用。

15

栈和字符串常量池的概念 图片描述


6
什么?我不明白这个为什么会有赞。你能解释一下你的回答吗? - Cᴏʀʏ
我认为预期的答案是,由于str1+str2未优化为内部字符串,因此与来自字符串池的字符串进行比较将导致错误条件。 - viki.omega9

3
让我们来看一些与 final 相关的字节码。
Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: ldc           #2                  // String string
       2: astore_3
       3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: aload_3
       7: ldc           #2                  // String string
       9: if_acmpne     16
      12: iconst_1
      13: goto          17
      16: iconst_0
      17: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
      20: return
}

0:2:时,"string"字符串被推入堆栈(来自常量池)并直接存储到本地变量concat中。您可以推断编译器在编译时创建(连接)"string"字符串本身。

final字节码

Compiled from "Main2.java"
public class Main2 {
  public Main2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: ldc           #2                  // String str
       2: astore_1
       3: ldc           #3                  // String ing
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
      17: aload_2
      18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
      21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      24: astore_3
      25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
      28: aload_3
      29: ldc           #9                  // String string
      31: if_acmpne     38
      34: iconst_1
      35: goto          39
      38: iconst_0
      39: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      42: return
}

这里有两个String常量:"str""ing",需要在运行时使用StringBuilder进行连接。

0
尽管如此,在使用Java的字符串字面值符号表示法创建时,如果该对象尚未存在于池中,它会自动调用intern()方法将其放入字符串池中。
为什么final有所不同?
编译器知道final变量永远不会改变,当我们添加这些final变量时,由于str1 + str2表达式的输出也永远不会改变,所以输出进入字符串池。因此,编译器在上述两个final变量的输出后调用了字符串的intern方法。对于非final变量,编译器不会调用intern方法。

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