深入理解 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
  1. 0 new #2 <java/lang/String> 表示是通过new创建了一个String对象,#2 就是经过类加载解析后的直接引用,解析前是java.lang.String
  2. ldc #3 <Hello> 这段指令会做两件事 判断 #3是否已经被解析成直接引用,如果没有则进行解析。然后检测字符串常量池中是否有Hello字符常量,如果有则直接获取引用,反之则在堆中创建一个Hello字符串对象,然后把引用放入字符串常量池
  3. invokespecial #4... 这行指令会执行String的构造方法进行初始化。
  4. 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";的字节码编译一模一样,所以说结果也是一模一样,会将Hello作为一个字符串整体,查看是否已存在字符常量,无则创建。
由此得出字符串通过+拼接,与直接完整创建没有任何差异。

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()返回。
那么我们结合之前讲到的,推断一下,这种场景会创建几个对象呢?

  1. 创建StringBuilder对象
  2. new String("He") 创建1或者2个对象,过程直接参考示例一
  3. + "llo" 创建0或1个对象,过程参考示例二和示例三
    总结: 如果字符串常量池中没有Hello,则会创建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" + "1"; 这种会直接创建一个值为11的字符串对象,并且会将该对象缓存到字符串常量池

  2. 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()作用: 主动将字符串自身的引用添加到字符串常量池,如果字符串常量池中已存在,则直接返回已存在的引用,不存在则添加。

posted @ 2022-05-16 12:05  Zero9501  阅读(1465)  评论(0编辑  收藏  举报