认为 new 方式创建了 1 个对象的人认为,new String 只是在堆上创建了一个对象,只有在使用 intern() 时才去常量池中查找并创建字符串。
认为 new 方式创建了 2 个对象的人认为,new String 会在堆上创建一个对象,并且在字符串常量池中也创建一个字符串。
认为 new 方式有可能创建 1 个或 2 个对象的人认为,new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象,如下图所示:
老王认为正确的答案:创建 1 个或者 2 个对象。
技术论证
解铃还须系铃人,回到问题的那个争议点上,new String 到底会不会在常量池中创建字符呢?我们通过反编译下面这段代码就可以得出正确的结论,代码如下:
public class StringExample { public static void main(String[] args) { String s1 = new String("javaer-wang"); String s2 = "wang-javaer"; String s3 = "wang-javaer"; } }
首先我们使用 javac StringExample.java 编译代码,然后我们再使用 javap -v StringExample 查看编译的结果,相关信息如下:
Classfile /Users/admin/github/blog-example/blog-example/src/main/java/com/example/StringExample.class Last modified 2020年4月16日; size 401 bytes SHA-256 checksum 89833a7365ef2930ac1bc3d7b88dcc5162da4b98996eaac397940d8997c94d8e Compiled from "StringExample.java" public class com.example.StringExample minor version: 0 major version: 58 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #16 // com/example/StringExample super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/lang/String #8 = Utf8 java/lang/String #9 = String #10 // javaer-wang #10 = Utf8 javaer-wang #11 = Methodref #7.#12 // java/lang/String."<init>":(Ljava/lang/String;)V #12 = NameAndType #5:#13 // "<init>":(Ljava/lang/String;)V #13 = Utf8 (Ljava/lang/String;)V #14 = String #15 // wang-javaer #15 = Utf8 wang-javaer #16 = Class #17 // com/example/StringExample #17 = Utf8 com/example/StringExample #18 = Utf8 Code #19 = Utf8 LineNumberTable #20 = Utf8 main #21 = Utf8 ([Ljava/lang/String;)V #22 = Utf8 SourceFile #23 = Utf8 StringExample.java { public com.example.StringExample(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=4, args_size=1 0: new #7 // class java/lang/String 3: dup 4: ldc #9 // String javaer-wang 6: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: astore_1 10: ldc #14 // String wang-javaer 12: astore_2 13: ldc #14 // String wang-javaer 15: astore_3 16: return LineNumberTable: line 5: 0 line 6: 10 line 7: 13 line 8: 16 } SourceFile: "StringExample.java"
备注:以上代码的运行也编译环境为 jdk1.8.0_101。
其中 Constant pool 表示字符串常量池,我们在字符串编译期的字符串常量池中找到了我们 String s1 = new String("javaer-wang"); 定义的“javaer-wang”字符,在信息 #10 = Utf8 javaer-wang 可以看出,也就是在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中,也就是说 new String 的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。
那么问题来了,以下这段代码的执行结果为 true 还是 false?
String s1 = new String("javaer-wang"); String s2 = new String("javaer-wang"); System.out.println(s1 == s2);
既然 new String 会在常量池中创建字符串,那么执行的结果就应该是 true 了。其实并不是,这里对比的变量 s1 和 s2 堆上地址,因为堆上的地址是不同的,所以结果一定是 false,如下图所示:
从图中可以看出 s1 和 s2 的引用一定是相同的,而 s3 和 s4 的引用是不同的,对应的程序代码如下:
public static void main(String[] args) { String s1 = "Java"; String s2 = "Java"; String s3 = new String("Java"); String s4 = new String("Java"); System.out.println(s1 == s2); System.out.println(s3 == s4); }
程序执行的结果也符合预期:
true
false

