深入理解 new String()
前言
在一些面试中,我们经常会看到一些关于String
的面试题,如String s1 = new String("a") + "b"
这个过程创建了几个字符串对象? 本章就详细来讲述一下不同方式创建String
的过程。
示例一
String s1 = new String("Hello");
首先这里由于使用new
来创建的对象,肯定会在堆中创建1个字符串对象,在Java中,所有的字符串字面量会缓存在字符串常量池
,所以每个字符串创建时都会在池内查看是否存在该字符串,如果不存在则会在堆中再新建1个字符串对象,然后将它的引用放入字符串常量池
。依次推断,这里会创建出2个字符串对象。
接下来我们通过插件来看一下这段代码的字节码执行流程,插件名称: jclasslib
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <Hello>
6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
9 astore_1
10 return
0 new #2 <java/lang/String>
表示是通过new
创建了一个String对象,#2
就是经过类加载解析后的直接引用,解析前是java.lang.String
。ldc #3 <Hello>
这段指令会做两件事 判断#3
是否已经被解析成直接引用,如果没有则进行解析。然后检测字符串常量池
中是否有Hello
字符常量,如果有则直接获取引用,反之则在堆中创建一个Hello
字符串对象,然后把引用放入字符串常量池
。invokespecial #4...
这行指令会执行String的构造方法进行初始化。9 astore_1
将创建好的字符串引用挂载到局部变量表中。
示例二
String s1 = "Hello";
这种方式,没有通过new
关键字,所以它的结果只会是创建0或者1个对象,如果字符串常量池
中不存在Hello
常量对象,则进行创建并且放入。如果已经存在,则直接返回引用。
0 ldc #2 <Hello>
2 astore_1
3 return
示例三
String s1 = "He" + "llo";
话不多说,直接看字节码
0 ldc #2 <Hello>
2 astore_1
3 return
与String s1 = "Hello";
的字节码编译一模一样,所以说结果也是一模一样,会将He
和llo
作为一个字符串整体,查看是否已存在字符常量,无则创建。
由此得出字符串通过+
拼接,与直接完整创建没有任何差异。
String s1 = "Hello";
String s2 = "He" + "llo";
String s3 = "H" + "e" + "l" + "l" + "o";
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//true
示例四
String s1 = new String("He") + "llo";
这种场景下的字符串拼接就会有些不同,我们先来看一下是如何拼接的,字节码如下:
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <He>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 ldc #8 <llo>
21 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
24 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
27 astore_1
28 return
很显然,Java在底层是通过StringBuilder
来进行字符串的拼接,通过append()
方法,最后直接toString()
返回。
那么我们结合之前讲到的,推断一下,这种场景会创建几个对象呢?
- 创建
StringBuilder
对象 new String("He")
创建1或者2个对象,过程直接参考示例一+ "llo"
创建0或1个对象,过程参考示例二和示例三
总结: 如果字符串常量池中没有He
和llo
,则会创建4个对象。如果已经存在常量,则创建2个对象
示例五 通过new 创建String的坑
String s1 = "1" + "1";
String s2 = "11";
System.out.println(s1 == s2);
通过以上示例二和三我们知道结果为true
,因为"1" + "1"会创建一个11
字符串对象,s2在创建时11
已经存在在字符串常量池
,所以直接获取该引用。
接下来我们换一种方式
String s1 = new String("1") + new String("1");
String s2 = "11";
System.out.println(s1 == s2);
那么这种方式的结果呢?大家可以试一下,并且思考一下,其实在示例四中已经有了答案,但这里我需要再强调一下,因为这是一个很容易忽略却很影响结果的要点。
结果:false
那么为什么呢? 原因如下:
-
"1" + "1";
这种会直接创建一个值为11
的字符串对象,并且会将该对象缓存到字符串常量池
。 -
new String("1") + new String("1");
而这种方式是通过StringBuilder
进行拼接的,他是通过创建两个1
字符串进行拼接,最终结果并没有创建11
这个字符串对象,所以这种方式创建后,字符串常量池
中并没有11
字符常量存在。
再来回顾一下String s1 = "1" + "1";
的字节码内容
0 ldc #2 <11>
2 astore_1
3 return
显然,这种方式最终创建了11
字符串对象,并且会缓存到字符串常量池
我们来再次看一下String s1 = new String("1") + new String("1");
的字节码文件,来回顾一下
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <1>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 new #4 <java/lang/String>
22 dup
23 ldc #5 <1>
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
31 invokevirtual #8 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
34 astore_1
35 return
看这段字节码,这段字节码执行了两次,分别创建两个String对象,值都是为1。并没有创建11
字符串对象。
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <1>
总结:
底层通过StringBuilder
拼接的字符串,最终的结果不会创建一个新的字符串对象。
详细描述:
String s1 = "1" + "1"
会直接创建一个值为11
字符串,然后将引用放入字符串常量池
String s2 = "11"
然后我们再声明一个值为11
字符串,其实它会在字符串常量池
中获取s1的引用,所以它们的引用相等。
String s1 = new String("1") + new String("1")
只会创建一个值为1
的字符串,然后将引用放入字符串常量池
。
String s2 = "11"
创建一个值为11
的字符串,由于字符串常量池
中不存在11
常量,则会重新创建一个新的11
字符串对象,然后将引用放入字符串常量池
,所以它们的引用不相等。
类似的案例如下:
String s1 = new String("1") + new String("1");
String s2 = new String("1") + "1";
String s3 = new StringBuilder("1").append("1").toString();
String s4 = "1" + "1";
System.out.println(s1 == s4); //false
System.out.println(s2 == s4);//false
System.out.println(s3 == s4);//false
结果统统为false
,因为它们底层都是由StringBuilder
拼接,不会产生11
字符串对象。
示例六
String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1 == s2);
通过示例五的学习,我们知道,这里应该会输出false
,但这里结果为true
,主要原因就是s1执行了intern()
方法.
intern()
作用: 主动将字符串自身的引用添加到字符串常量池
,如果字符串常量池中已存在,则直接返回已存在的引用,不存在则添加。