new String("test")到底创建了几个对象?
new String("test")到底创建了几个对象?
我相信不少学 Java 的小伙伴都曾被这个高频的 Java 面试题困扰过,但大家的回答都多多少少有些出入,但是总的来说,关于 new String("test") 创建对象个数的答案有 3 种:
有的人说创建了 1 个对象;有的人说创建了 2 个对象;还有的人说创建了 1 个或 2 个对象。
其实这几个答案的关键争议点在「字符串常量池」上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串。
我们可以分几步来学习这个问题
第一步
那我们就先来了解一下字符串常量池。字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区。
对于字符串的2种创建方式,我们做如下了解: (下图通过ProcessOn网站完成,有兴趣的小伙伴可以注册一个使用,这是网站链接:https://www.processon.com/i/5fb4e8516376895bf971d05b)
方式1:
String str = "test"; 这种方法创建字符串对象的时候,jvm首先会检查 字符串常量池 中是否存在该字符串的对象,如果已经存在,那么就不会在字符串常量池中再创建了,直接 返回该字符串常量池内存中的内存地址,如果该字符串还不存在字符串常量池中,那么就会在字符串常量池中先创建该字符串的对象,然后在返回其内存地址。
public class Test01 {
public static void main(String[] args) {
String s1 = "Test";
String s2 = "Test";
System.out.println(s1 == s2);
}
}
以上程序的执行结果为:true,说明变量 s1 和变量 s2 指向的是同一个地址。
方式2:
new String("test");这种方式创建字符串对象的时候,jvm首先会检查字符串常量池中是否存在 "test"的字符串,如果已经存在,则不会在字符串常量池中创建了,如果还没有存在,那么就会在字符串常量池中创建 "test"字符串对象,然后还会去堆内存中再创建一份字符串的对象(时刻谨记:凡是以new关键字创建的对象,jvm都会在堆内存中开辟一个新的空间,创建一个新的对象),把字符串常量池中的 "test"字符串内容拷贝至内存中的字符串对象,然后返回堆内存中字符串对象的内存地址。
以上说法可以通过如下代码进行证明:
public class Test02 {
public static void main(String[] args) {
String s1 = new String("Test");
String s2 = new String("Test");
System.out.println(s1 == s2);
}
}
以上程序的执行结果为:false,是因为堆上的 对象s1 和对象s2 的地址不同。
第二步
探究 new String("test") 到底会不会在常量池中创建字符呢?我们通过反编译下面这段代码就可以得出正确的结论,代码如下:
public class Test02 {
public static void main(String[] args) {
String s1 = new String("Test");
String s2 = "Test";
}
}
首先我们使用 javac Test02.java 编译代码,然后我们再使用 javap -v Test02 查看编译的结果,相关信息如下:我的环境为 "1.8.0_121".
警告: 二进制文件Test02包含com.cqyti.stx.str.Test02
Classfile /E:/IDEA project/Training/src/com/cqyti/stx/str/Test02.class
Last modified 2021-9-19; size 362 bytes
MD5 checksum 0e430f2c1b03d2597be3cf5d03f449d3
Compiled from "Test02.java"
public class com.cqyti.stx.str.Test02
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool: // Constant pool是常量池
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // java/lang/String
#3 = String #17 // Test
#4 = Methodref #2.#18 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = Class #19 // com/cqyti/stx/str/Test02
#6 = Class #20 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Test02.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Utf8 java/lang/String
#17 = Utf8 Test
#18 = NameAndType #7:#21 // "<init>":(Ljava/lang/String;)V
#19 = Utf8 com/cqyti/stx/str/Test02
#20 = Utf8 java/lang/Object
#21 = Utf8 (Ljava/lang/String;)V
{
public com.cqyti.stx.str.Test02();
descriptor: ()V
flags: 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 7: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String Test
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: ldc #3 // String Test
12: astore_2
13: return
LineNumberTable:
line 42: 0
line 43: 10
line 45: 13
}
SourceFile: "Test02.java"
其中 Constant pool 表示字符串常量池,我们在字符串编译期的字符串常量池中找到了我们 String s1 = new String("Test"); 定义的"Test"字符,在信息 #17 = Utf8 Test 可以看出,也就是在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中,也就是说 new String 的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。
再进一步
我们知道 String 是 final 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,例如以下代码:
public static void main(String[] args) {
String a = "test";
String b = "test";
String c = new String("test");
String d = "te" + "st";
String e = "te";
String f = "st";
String g = e + f;
System.out.println(a == b);// true
System.out.println(a.equals(b));// true
System.out.println(c == b);// false
System.out.println(a.equals(c));// true
System.out.println(d == a);// true
System.out.println(d == b);// true
System.out.println(a.equals(d));// true
System.out.println(g == a);// false
System.out.println(a.equals(g));// true
}
按照 String 不能被修改的思想来看,d 应该会在字符串常量池创建两个字符串"te" 和 "st",d == a 与 d == b 的结果也应该是 false,但其实不是,他们的结果都是 true,这是编译器优化的功劳。
同样我们使用 javac Test02.java 先编译代码,再使用 javap -c Test02 命令查看编译的代码如下:
警告: 二进制文件Test02包含com.cqyti.stx.str.Test02
Compiled from "Test02.java"
public class com.cqyti.stx.str.Test02 {
public com.cqyti.stx.str.Test02();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String test
2: astore_1
3: ldc #2 // String test
5: astore_2
6: new #3 // class java/lang/String
9: dup
10: ldc #2 // String test
12: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
15: astore_3
16: ldc #2 // String test
18: astore 4
20: ldc #5 // String te
22: astore 5
24: ldc #6 // String st
26: astore 6
28: new #7 // class java/lang/StringBuilder
31: dup
32: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
35: aload 5
37: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
40: aload 6
42: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
45: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
48: astore 7
50: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
53: aload_1
54: aload_2
55: if_acmpne 62
58: iconst_1
59: goto 63
62: iconst_0
63: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
66: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
69: aload_1
70: aload_2
71: invokevirtual #13 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
74: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
77: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
80: aload_3
81: aload_2
82: if_acmpne 89
85: iconst_1
86: goto 90
89: iconst_0
90: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
93: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
96: aload_1
97: aload_3
98: invokevirtual #13 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
101: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
104: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
107: aload 4
109: aload_1
110: if_acmpne 117
113: iconst_1
114: goto 118
117: iconst_0
118: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
121: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
124: aload_1
125: aload 4
127: invokevirtual #13 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
130: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
133: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
136: aload 7
138: aload_1
139: if_acmpne 146
142: iconst_1
143: goto 147
146: iconst_0
147: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
150: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
153: aload_1
154: aload 7
156: invokevirtual #13 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
159: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
162: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
165: aload 4
167: aload_2
168: if_acmpne 175
171: iconst_1
172: goto 176
175: iconst_0
176: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
179: return
}
从 Code 16: ldc #2 行可以看出字符串都被编译器优化成了字符串"test"了。
总结
我们可以通过 javap -c XXX 查看文件字节码信息, javap -v XXX 的方式查看编译结果的过程中发现 new String("test") 首次会在字符串常量池中创建此字符串,那也就是说,通过 new 创建字符串的方式可能会创建 1 个或 2 个对象,如果常量池中已经存在此字符串只会在堆上创建一个变量,并指向字符串常量池中的值,如果字符串常量池中没有相关的字符,会先创建字符串在返回此字符串的引用给堆空间的变量。所以在学习过程中我们需要具体问题,具体分析。