用==比较在Java中声明为最终的string

我有一个关于Java中string的简单问题。 以下简单代码段连接两个string,然后将它们与==进行比较。

 String str1="str"; String str2="ing"; String concat=str1+str2; System.out.println(concat=="string"); 

比较expression式concat=="string"返回false (我理解equals()== )之间的区别。


当这两个string被声明为final时候,

 final String str1="str"; final String str2="ing"; String concat=str1+str2; System.out.println(concat=="string"); 

比较expression式concat=="string" ,在这种情况下返回true 。 为什么final有所作为? 是否需要对实习生池做些什么,或者我只是被误导了?

当您将一个String不可变 )variables声明为final ,并用一个编译时常量expression式初始化它时,它也会变成一个编译时常量expression式,并且它的值由编译器在内部使用。 因此,在第二个代码示例中,在内联值之后,编译器将string连接转换为:

 String concat = "str" + "ing"; // which then becomes `String concat = "string";` 

当与"string"相比时,会给你true ,因为string文字是interned

从JLS§4.12.4 – finalvariables :

原始types或types为Stringvariables是final并且使用编译时常量expression式(第15.28节)进行初始化,该variables称为常量variables

同样来自JLS§15.28 – 常量expression式:

编译时间常量expression式的String总是“interned” ,以共享唯一的实例,使用方法String#intern()


在您的第一个代码示例中, Stringvariables不是final情况并非如此。 所以,它们不是一个编译时常量expression式。 这里的连接操作将被延迟到运行时,从而导致创build一个新的String对象。 您可以通过比较两个代码的字节代码来validation。

第一个代码示例(非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存储在两个单独的variables中,并使用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 

所以它直接内联最后一个variables,以便在编译时创buildString string ,该string在步骤0ldc操作加载。 然后在步骤7通过ldc操作加载第二个string文字。 它不涉及在运行时创build任何新的String对象。 string在编译时已经知道了,并且被实现了。

根据我的研究,所有final String都是用Java编写的。 从其中一篇博客文章:

所以,如果你真的需要比较两个string使用==或!=请确保在进行比较之前调用String.intern()方法。 否则,总是比较喜欢使用String.equals(String)进行string比较。

所以这意味着如果你调用String.intern()你可以使用==运算符比较两个string。 但是在这里String.intern()并不是必须的,因为在Java中final String是在内部实现的。

你可以find更多的信息string比较使用==运算符和Javadoc String.intern()方法。

另请参阅此Stackoverflowpost了解更多信息。

如果你看看这个方法

 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 ... 

所以如果string不是最终的,编译器将不得不使用StringBuilder来连接str1str2

 String concat=str1+str2; 

将编译到

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

这意味着concat将在运行时创build,所以不会来自String池。


另外,如果string是最终的,那么编译器可以假定它们永远不会改变,所以不是使用StringBuilder而是可以安全地连接它的值

 String concat = str1 + str2; 

可以改成

 String concat = "str" + "ing"; 

并串联而成

 String concat = "string"; 

这意味着concate将成为sting文字,它将被放入string池中,然后在if语句中与该池中的相同string文字进行比较。

堆栈和stringconts池的概念 在这里输入图像描述

让我们看看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 "string"被压入堆栈(从常量池)并直接存储到本地variablesconcat 。 您可以推断编译器在编译时正在创build(连接) String "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连接起来。

但是,当使用Java的String文字符号创build时,它会自动调用intern()方法将该对象放入string池中,只要该对象不在池中。

为什么最后会有所作为?

编译器知道最终variables永远不会改变,当我们添加这些最终variables的输出到string池,因为str1 + str2expression式输出也永远不会改变,所以最后编译器调用后,上述两个最终variables的输出中间方法。 在非最终variables编译器的情况下不要调用实习方法。