别再问我 new 字符串创建了几个对象了!我来证明给你看!

2024-02-10 22:18

本文主要是介绍别再问我 new 字符串创建了几个对象了!我来证明给你看!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

如何证明 new String 创建了 N 个对象?

我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有拿出干掉对方的证据,这就让我们这帮吃瓜群众们陷入了两难之中,不知道到底该信谁得。

但是今天,老王就斗胆和大家聊聊这个话题,顺便再拿出点证据

以目前的情况来看,关于 new String("xxx") 创建对象个数的答案有 3 种:

  1. 有人说创建了 1 个对象;
  2. 有人说创建了 2 个对象;
  3. 有人说创建了 1 个或 2 个对象。

而出现多个答案的关键争议点在「字符串常量池」上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串。

那我们就先来说说这个「字符串常量池」。

字符串常量池

字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。

字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区,对于直接赋值的字符串(String s=“xxx”)来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图所示:

![字符串常量池示意图.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4ubmxhcmsuY29tL3l1cXVlLzAvMjAyMC9wbmcvOTI3OTEvMTU4NzAzNzc4ODY5NC00YThhMWE1Zi03YWNhLTQ1ZDAtYjQyYS0wODRlODA5YTQ3ZmYucG5n?x-oss-process=image/format,png#align=left&display=inline&height=301&margin=[object Object]&name=字符串常量池示意图.png&originHeight=301&originWidth=464&size=23923&status=done&style=none&width=464)
以上说法可以通过如下代码进行证明:

public class StringExample {public static void main(String[] args) {String s1 = "Java";String s2 = "Java";System.out.println(s1 == s2);}
}

以上程序的执行结果为:true,说明变量 s1 和变量 s2 指向的是同一个地址。

在这里我们顺便说一下字符串常量池的再不同 JDK 版本的变化。

常量池的内存布局

JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上

JDK 1.7 内存布局如下图所示:
![JDK 1.7 内存布局.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4ubmxhcmsuY29tL3l1cXVlLzAvMjAyMC9wbmcvOTI3OTEvMTU4NzAzOTE0NzYzNi1lNzEzZjMwNy0yMTQ2LTQ1OWItOTJmNi1jMDE5OTNhZDMwNmMucG5n?x-oss-process=image/format,png#align=left&display=inline&height=311&margin=[object Object]&name=JDK 1.7 内存布局.png&originHeight=311&originWidth=631&size=38867&status=done&style=none&width=631)
JDK 1.8 内存布局如下图所示:
![JDK 1.8 内存布局.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4ubmxhcmsuY29tL3l1cXVlLzAvMjAyMC9wbmcvOTI3OTEvMTU4NzAzOTU5Njc5NC1kMDBhNzI1MC0xMTA4LTQ2NzgtYmI1MC0yYWVmNmQwNDAyZmEucG5n?x-oss-process=image/format,png#align=left&display=inline&height=464&margin=[object Object]&name=JDK 1.8 内存布局.png&originHeight=464&originWidth=538&size=44373&status=done&style=none&width=538)
JDK 1.8 与 JDK 1.7 最大的区别是 JDK 1.8 将永久代取消,并设立了元空间。官方给的说明是由于永久代内存经常不够用或发生内存泄露,会爆出 java.lang.OutOfMemoryError: PermGen 的异常,所以把将永久区废弃而改用元空间了,改为了使用本地内存空间,官网解释详情:http://openjdk.java.net/jeps/122

答案解密

认为 new 方式创建了 1 个对象的人认为,new String 只是在堆上创建了一个对象,只有在使用 intern() 时才去常量池中查找并创建字符串。

认为 new 方式创建了 2 个对象的人认为,new String 会在堆上创建一个对象,并且在字符串常量池中也创建一个字符串。

认为 new 方式有可能创建 1 个或 2 个对象的人认为,new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象,如下图所示:
![new 字符串常量池.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4ubmxhcmsuY29tL3l1cXVlLzAvMjAyMC9wbmcvOTI3OTEvMTU4NzA0MTU3MzcyNy01YjA2ODMzZS01NmYzLTQyZmItYWZiYy0xNzM1ZjQ4ZDZkZmMucG5n?x-oss-process=image/format,png#align=left&display=inline&height=321&margin=[object Object]&name=new 字符串常量池.png&originHeight=321&originWidth=527&size=27796&status=done&style=none&width=527)

老王认为正确的答案:创建 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.classLast modified 2020416; size 401 bytesSHA-256 checksum 89833a7365ef2930ac1bc3d7b88dcc5162da4b98996eaac397940d8997c94d8eCompiled from "StringExample.java"
public class com.example.StringExampleminor version: 0major version: 58flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #16                         // com/example/StringExamplesuper_class: #2                         // java/lang/Objectinterfaces: 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: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=3, locals=4, args_size=10: new           #7                  // class java/lang/String3: dup4: ldc           #9                  // String javaer-wang6: invokespecial #11                 // Method java/lang/String."<init>":(Ljava/lang/String;)V9: astore_110: ldc           #14                 // String wang-javaer12: astore_213: ldc           #14                 // String wang-javaer15: astore_316: returnLineNumberTable:line 5: 0line 6: 10line 7: 13line 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,如下图所示:

![字符串引用.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4ubmxhcmsuY29tL3l1cXVlLzAvMjAyMC9wbmcvOTI3OTEvMTU4NzA0MzUwNDAyMS00MzFkYTMyZS0xYjE0LTQ1NmEtOGZmNy0zZGJmYzM3Yjg2NTIucG5n?x-oss-process=image/format,png#align=left&display=inline&height=394&margin=[object Object]&name=字符串引用.png&originHeight=394&originWidth=593&size=41183&status=done&style=none&width=593)
从图中可以看出 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

扩展知识

我们知道 String 是 final 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,例如以下代码:

public static void main(String[] args) {String s1 = "abc";String s2 = "ab" + "c";String s3 = "a" + "b" + "c";System.out.println(s1 == s2);System.out.println(s1 == s3);
}

按照 String 不能被修改的思想来看,s2 应该会在字符串常量池创建两个字符串“ab”和“c”,s3 会创建三个字符串,他们的引用对比结果也一定是 false,但其实不是,他们的结果都是 true,这是编译器优化的功劳。

同样我们使用 javac StringExample.java 先编译代码,再使用 javap -c StringExample 命令查看编译的代码如下:

警告: 文件 ./StringExample.class 不包含类 StringExample
Compiled from "StringExample.java"
public class com.example.StringExample {public com.example.StringExample();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: ldc           #7                  // String abc2: astore_13: ldc           #7                  // String abc5: astore_26: ldc           #7                  // String abc8: astore_39: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;12: aload_113: aload_214: if_acmpne     2117: iconst_118: goto          2221: iconst_022: invokevirtual #15                 // Method java/io/PrintStream.println:(Z)V25: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;28: aload_129: aload_330: if_acmpne     3733: iconst_134: goto          3837: iconst_038: invokevirtual #15                 // Method java/io/PrintStream.println:(Z)V41: return
}

从 Code 3、6 可以看出字符串都被编译器优化成了字符串“abc”了。

总结

本文我们通过 javap -v XXX 的方式查看编译的代码发现 new String 首次会在字符串常量池中创建此字符串,那也就是说,通过 new 创建字符串的方式可能会创建 1 个或 2 个对象,如果常量池中已经存在此字符串只会在堆上创建一个变量,并指向字符串常量池中的值,如果字符串常量池中没有相关的字符,会先创建字符串在返回此字符串的引用给堆空间的变量。我们还将了字符串常量池在 JDK 1.7 和 JDK 1.8 的变化以及编译器对确定字符串的优化,希望能帮你正在的理解字符串的比较。

最后的话
原创不易,本篇近 3000 的文字描述,以及大量精美的图片,耗费了作者大概 5 个多小时的时间,写作是一件很酷,并且能帮助他人的事,作者希望一直能坚持下去。如果觉得有用,请随手点击一个赞吧,谢谢

这篇关于别再问我 new 字符串创建了几个对象了!我来证明给你看!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/698167

相关文章

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Python和Pyecharts创建交互式地图

《使用Python和Pyecharts创建交互式地图》在数据可视化领域,创建交互式地图是一种强大的方式,可以使受众能够以引人入胜且信息丰富的方式探索地理数据,下面我们看看如何使用Python和Pyec... 目录简介Pyecharts 简介创建上海地图代码说明运行结果总结简介在数据可视化领域,创建交互式地

Java对象转换的实现方式汇总

《Java对象转换的实现方式汇总》:本文主要介绍Java对象转换的多种实现方式,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Java对象转换的多种实现方式1. 手动映射(Manual Mapping)2. Builder模式3. 工具类辅助映

Java中字符串转时间与时间转字符串的操作详解

《Java中字符串转时间与时间转字符串的操作详解》Java的java.time包提供了强大的日期和时间处理功能,通过DateTimeFormatter可以轻松地在日期时间对象和字符串之间进行转换,下面... 目录一、字符串转时间(一)使用预定义格式(二)自定义格式二、时间转字符串(一)使用预定义格式(二)自

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

一文详解如何在Python中从字符串中提取部分内容

《一文详解如何在Python中从字符串中提取部分内容》:本文主要介绍如何在Python中从字符串中提取部分内容的相关资料,包括使用正则表达式、Pyparsing库、AST(抽象语法树)、字符串操作... 目录前言解决方案方法一:使用正则表达式方法二:使用 Pyparsing方法三:使用 AST方法四:使用字

Java字符串处理全解析(String、StringBuilder与StringBuffer)

《Java字符串处理全解析(String、StringBuilder与StringBuffer)》:本文主要介绍Java字符串处理全解析(String、StringBuilder与StringBu... 目录Java字符串处理全解析:String、StringBuilder与StringBuffer一、St

Python中判断对象是否为空的方法

《Python中判断对象是否为空的方法》在Python开发中,判断对象是否为“空”是高频操作,但看似简单的需求却暗藏玄机,从None到空容器,从零值到自定义对象的“假值”状态,不同场景下的“空”需要精... 目录一、python中的“空”值体系二、精准判定方法对比三、常见误区解析四、进阶处理技巧五、性能优化

Python中的魔术方法__new__详解

《Python中的魔术方法__new__详解》:本文主要介绍Python中的魔术方法__new__的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、核心意义与机制1.1 构造过程原理1.2 与 __init__ 对比二、核心功能解析2.1 核心能力2.2

MySQL更新某个字段拼接固定字符串的实现

《MySQL更新某个字段拼接固定字符串的实现》在MySQL中,我们经常需要对数据库中的某个字段进行更新操作,本文就来介绍一下MySQL更新某个字段拼接固定字符串的实现,感兴趣的可以了解一下... 目录1. 查看字段当前值2. 更新字段拼接固定字符串3. 验证更新结果mysql更新某个字段拼接固定字符串 -