流程图详解 new String(“abc“) 创建了几个字符串对象

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
云解析DNS,个人版 1个月
简介: 这道题是我之前的面试题文章《Java 基础高频面试题(2021年最新版)》里的第10题,今天通过字节码和流程图来跟大家详解一下完整的执行过程。同时也会涉及一些字符串常量池的相关知识,这块内容网上现在的说法有太多错误了

前言


这道题是我之前的面试题文章《Java 基础高频面试题(2021年最新版)》里的第10题,今天通过字节码和流程图来跟大家详解一下完整的执行过程。

同时也会涉及一些字符串常量池的相关知识,这块内容网上现在的说法有太多错误了。


答案


首先直接说答案,一个比较合理的答案是:一个或者两个字符串对象,通常这个也是面试官想要听到的答案。

首先,new string 这边由于 new 关键字,所以这边肯定会在堆中新建一个字符串对象。

其次,如果字符串常量池中不存在 jionghuiequals比较)这个字符串,则会在字符串常量池中创建一个字符串对象。

注意:这边说的在字符串常量池创建对象,最终对象还是在堆中创建,字符串常量池只放引用。


例子1String str1 = new String("jionghui")


本例子按照字符串常量池中不存在 jionghui 字符串来说。

该代码编译后其字节码如下:

 0 new #2 <java/lang/String>
 3 dup
 4 ldc #3 <jionghui>
 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
 9 astore_1
10 return

接下来我们解释下这些字节码


1)#2#3#4


字节码中这些带# 号的数字,是我们常量池里面的符号引用,这些符号引用会在类加载的解析阶段被解析为直接引用,直接引用可以理解为就是对象在内存中的地址。

这些符号引用对应的内容在后面已经给你列出来了。

#2 这边对应的是 java.lang.String Class

#3 对应的 jionghui 字符串

#4 对应的 String 的初始化方法


2)new


new 关键字就是新建对象的意思,这边相当于会新建一个String 对象,但是此时还未初始化,是一个空对象。同时,这个字节码会将创建的对象的引用存放到操作数栈的栈顶。

执行完该指令后的结构:

image.png

3)dup


复制的意思,这边就是复制一份栈顶的元素。

这边在栈顶复制一个 String 的引用是因为后续调用 String 的初始化方法会消耗掉栈里的一个引用,所以这边提前复制一份出来,最后才有引用可以赋值给局部变量表的str1

执行完该指令后的结构:image.png


4)ldc


intfloatString型常量值从常量池中推送至栈顶,这个地方 ldc 指令会附带另外一个功能:触发符号引用解析为直接引用。


我们上文说过符号引用会在解析阶段被解析成直接引用,但是有一些特例。字符串对象就是一个特例,字符串对象不会在解析阶段就将符号引用解析成直接引用,而是等到某个合适的时机才去解析,这边的 ldc 就是这个时机。

PS:下面的例子5会验证这个说法。


因此ldc 在这边会做两件事:


1、判断符号引用是否已经解析成了直接引用,如果没有,则会进行解析:判断 jionghui 字符串是否已经在字符串常量池存在,如果存在则将符号引用解析成字符串常量池的引用;如果不存在,则会在字符串常量池中创建一个jionghui 字符串对象,然后同样将符号引用解析成字符串常量池的引用。

2、将对应的字符串常量池推送到栈顶。

执行完该指令后的结构:

image.png

5)invokespecial


调用超类构造方法,实例初始化方法,私有方法。


在这边用于调用String 的初始化方法,我们上面通过 new 关键词创建的是个空对象,还未进行初始化。


这边初始化会使用到我们栈顶的两个元素,一个元素指向我们要初始化的对象,另一个元素指向我们初始化使用的参数。


这边初始化完毕后,这个空字符串对象会被初始化成 jionghui 字符串对象。

执行完该指令后的结构:


6)astore_1


将栈顶引用元素存到指定本地变量。

这边最后将栈顶的这个引用存放到本地变量表找那个的 str1 变量。

执行完该指令后的结构:

image.png

执行完毕后,最终如上图所示。可以看到最终就是创建了两个对象,一个是是通过new string 创建出来的这个对象,它的引用被复赋值给str1,另外一个是在常量池里创建的字符串对象。


例子2String str2 = "jionghui"


这个例子就是例子1的简版,去掉了 new String 的过程,其他基本一样。

执行结束的内存结构如下图所示:image.png


例子3String str3 = "jiong" + "hui";


该例子在编译后,这2个字符串会被自动合并成 jionghui,所以最终跟例子2完全一样,编译后的字节码都是完全一样的。

执行结束的内存结构如下图所示:

image.png

例子4String str4 = new String("jiong") + "hui"


核心流程如下:


1)双引号修饰的字面量 jiong hui 分别会在字符串常量池中创建字符串对象

2new String 关键字会再创建一个 jiong 字符串对象

3)最后这个字符串拼接,这个地方不看字节码的话很难看出究竟是怎么拼接的,通过字节码一下子就看出来了,这边是通过 StringBuilder 来进行字符串的拼接操作,先创建了一个StringBuilder,然后 append("jiong"),然后 append("hui"),最后执行toString 返回,这边 toString 底层是通过 new String 方法返回,所以最终这边拼接也会创建一个新的字符串。


字节码如下:用到的命令都是上文提过的。

 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 <jiong>
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 <hui>
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

执行结束的内存结构如下图所示:

image.png

例子5intern 测试1


String str5 = new String("1") + new String("1");
str5.intern();
String str6 = "11";
System.out.println(str5 == str6);

intern:如果字符串常量池中存在当前字符串, 则返回常量池中的字符串引用。否则, 将该字符串放入常量池,然后返回该字符串对象的引用。

intern JDK6 JDK7 及之后的版本有些不同,这边会简单说下不同的地方。


JDK7下的核心流程如下:


1)双引号修饰的字面量 1 会在字符串常量池中创建字符串对象,这边有2个字面量1,但是只会创建1次,另一个直接复用

2)两个 new String 创建了2个字符串对象 1

3)字符串拼接通过 StringBuilder 创建出1个新的字符串对象11,并将引用赋值给 str5

4str5 调用 intern 方法,检查到字符串常量池还没有字符串11,则将字符串对象放入常量池,此时字符串常量池中的 11 就是 str5 指向的字符串对象

5)双引号修饰的字面量 11 检查到字符串常量池中已经存在字符串 11,则直接使用字符串常量池中的对象,所以 str6 被赋值为字符串常量池中的对象引用,也就是str5的引用

6)输出结果为 true

执行结束的内存结构如下图所示:

image.png

JDK6 下的流程有什么不同呢,主要在于 JDK6 版本还存在永久代的概念,字符串常量池指向的字符串对象在JDK6 中是在永久代创建的,JDK7才被移动到堆中。

所以当执行str5.intern 时,发现永久代中没有字符串11,则会在永久代创建字符串对象11,后续的 str6 也是指向永久代的字符串对象。所以,此时str5 str6 指向的不同对象。

因此,JDK6的输出结果为 false

执行结束的内存结构如下图所示:

image.png

验证字符串对象在运行中在解析符号引用

这个例子还能验证我们上面说的:字符串的符号引用在运行阶段才被解析成直接引用的说法。


我们假设字符串的符号引用也是在类加载的解析阶段就解析成直接引用了,那么这个例子的流程如下(JDK7及之后版本):


1)解析阶段,双引号修饰的字面量 1 11 会在字符串常量池中创建字符串对象

2)两个 new String 创建了2个字符串对象 1

3)字符串拼接通过 StringBuilder 创建出1个新的字符串对象11,并将引用赋值给 str5

4str5 调用 intern 方法,检查到字符串常量池存在字符串11,则不做任何操作

5str6 被赋值为字符串常量池中的对象引用,此时str6 str5 指向的是不同的字符串对象

6)输出结果为 false

本例在JDK7及之后版本的输出结果为 true,验证了我们的说法。

字符串常量池中的字符串对象使用懒加载在 JVM 源码中是有明确注释的,同时 R 大也在某论坛上说过。


例子6intern 测试2


这个例子就是将例子523行代码调换了下顺序,验证一下 intern 方法的返回值。

String str7 = new String("1") + new String("1");
String str8 = "11";
String str9 = str7.intern();
System.out.println(str7 == str8);
System.out.println(str8 == str9);

核心流程如下:


1)双引号修饰的字面量 1 会在字符串常量池中创建字符串对象,这边有2个字面量1,但是只会创建1次,另一个直接复用

2)两个 new String 创建了2个字符串对象 1

3)字符串拼接通过 StringBuilder 创建出1个新的字符串对象11,并将引用赋值给 str7

3)双引号修饰的字面量 11 会在字符串常量池中创建字符串对象,并将引用赋值给str8

4str7 调用 intern 方法,检查到字符串常量池存在字符串11,则不做任何操作,同时返回字符串常量池的引用,并赋值给 str9,也就是 str8 指向的引用

5)输出结果为 false true

执行结束的内存结构如下图所示:

image.png


推荐阅读


全网最实用的 IDEA Debug 调试技巧(超详细案例)

Java 基础高频面试题(2021年最新版)

Java 集合框架高频面试题(2021年最新版)

面试必问的 Spring,你懂了吗?

面试必问的 MySQL,你懂了吗?

相关文章
|
9天前
|
存储 程序员 API
八股day05_API、String对象和集合
day05_API、String对象和集合
|
3天前
|
存储 Java
构造String问题之在JDK 9及更高版本中,直接访问String对象的coder和value属性,如何实现
构造String问题之在JDK 9及更高版本中,直接访问String对象的coder和value属性,如何实现
|
1月前
|
Java 开发者 Python
Python中,字符串(String)是一种不可变的数据类型
Python中,字符串(String)是一种不可变的数据类型
|
1月前
|
存储 NoSQL Redis
Redis07命令-String类型字符串,不管是哪种格式,底层都是字节数组形式存储的,最大空间不超过512m,SET添加,MSET批量添加,INCRBY age 2可以,MSET,INCRSETEX
Redis07命令-String类型字符串,不管是哪种格式,底层都是字节数组形式存储的,最大空间不超过512m,SET添加,MSET批量添加,INCRBY age 2可以,MSET,INCRSETEX
|
1月前
|
存储
数据存储之数组的特点,长度固定,适应变化需求,集合类特点是空间可变,ArrayList泛型,ArrayList<String> array = new ArrayList<String>()
数据存储之数组的特点,长度固定,适应变化需求,集合类特点是空间可变,ArrayList泛型,ArrayList<String> array = new ArrayList<String>()
遍历字符串,String line = xxx for(int i = 0;i<line.length();i++){system.out.println(line.chartAt(i)); 单个
遍历字符串,String line = xxx for(int i = 0;i<line.length();i++){system.out.println(line.chartAt(i)); 单个
String对象的特点,new创建的字符串对象地址值不同,String s3 = “abc“; s4=“abc“ sout(s1 == s2)比较地址和内容,s1.equals(s3)比较内容
String对象的特点,new创建的字符串对象地址值不同,String s3 = “abc“; s4=“abc“ sout(s1 == s2)比较地址和内容,s1.equals(s3)比较内容
|
2月前
|
Java UED
Java中String强转int:一种常见的错误和解决方法
在Java中将非数字字符串转换为整数会导致`NumberFormatException`。要解决这个问题,可以使用`try-catch`捕获异常,正则表达式验证数字格式,或利用异常信息提供错误提示。例如,`Integer.parseInt()`会因遇到非数字字符如`&quot;123abc&quot;`而抛出异常,但通过异常处理或正则`\\d+`可确保安全转换。记得在编程时避免直接强转,以防止程序异常中断。
|
3天前
|
前端开发 Java
成功解决:java.lang.String cannot be cast to java.lang.Integer
这篇文章记录了作者在使用Axios二次封装时遇到的一个Java类型转换问题,即前端传递的字符串参数不能直接转换为Integer类型,文章提供了正确的转换方法来解决这个问题。
成功解决:java.lang.String cannot be cast to java.lang.Integer
|
6天前
|
Java Android开发
解决Android编译报错:Unable to make field private final java.lang.String java.io.File.path accessible
解决Android编译报错:Unable to make field private final java.lang.String java.io.File.path accessible
22 1