别再问我 new 字符串创建了几个对象了!我来证明给你看!
云栖号资讯:【点击查看更多行业资讯】
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!
我想所有 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")来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图所示:
以上说法可以通过如下代码进行证明:
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.8 内存布局如下图所示:
JDK 1.8 与 JDK 1.7 最大的区别是 JDK 1.8 将永久代取消,并设立了元空间。官方给的说明是由于永久代内存经常不够用或发生内存泄露,会爆出 java.lang.OutOfMemoryError: PermGen 的异常,所以把将永久区废弃而改用元空间了,改为了使用本地内存空间。
答案解密
认为 new 方式创建了 1 个对象的人认为,new String 只是在堆上创建了一个对象,只有在使用 intern() 时才去常量池中查找并创建字符串。
认为 new 方式创建了 2 个对象的人认为,new String 会在堆上创建一个对象,并且在字符串常量池中也创建一个字符串。
认为 new 方式有可能创建 1 个或 2 个对象的人认为,new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象,如下图所示:
老王认为正确的答案:创建 1 个或者 2 个对象。
技术论证
解铃还须系铃人,回到问题的那个争议点上,new String 到底会不会在常量池中创建字符呢?我们通过反编译下面这段代码就可以得出正确的结论,代码如下:
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 查看编译的结果,相关信息如下:
Last modified 2020年4月16日; size 401 bytes SHA-256 checksum 89833a7365ef2930ac1bc3d7b88dcc5162da4b98996eaac397940d8997c94d8e Compiled from "StringExample.java" public class com.example.StringExample minor version: 0 major version: 58 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #16 // com/example/StringExample super_class: #2 // java/lang/Object interfaces: 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: ()V flags: (0x0001) 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 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=4, args_size=1 0: new #7 // class java/lang/String 3: dup 4: ldc #9 // String javaer-wang 6: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: astore_1 10: ldc #14 // String wang-javaer 12: astore_2 13: ldc #14 // String wang-javaer 15: astore_3 16: return LineNumberTable: line 5: 0 line 6: 10 line 7: 13 line 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 s2 = new String("javaer-wang"); System.out.println(s1 == s2);
既然 new String 会在常量池中创建字符串,那么执行的结果就应该是 true 了。其实并不是,这里对比的变量 s1 和 s2 堆上地址,因为堆上的地址是不同的,所以结果一定是 false,如下图所示:
从图中可以看出 s1 和 s2 的引用一定是相同的,而 s3 和 s4 的引用是不同的,对应的程序代码如下:
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 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,例如以下代码:
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 命令查看编译的代码如下:
Compiled from "StringExample.java" public class com.example.StringExample { public com.example.StringExample(); 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 #7 // String abc 2: astore_1 3: ldc #7 // String abc 5: astore_2 6: ldc #7 // String abc 8: astore_3 9: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 12: aload_1 13: aload_2 14: if_acmpne 21 17: iconst_1 18: goto 22 21: iconst_0 22: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V 25: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 28: aload_1 29: aload_3 30: if_acmpne 37 33: iconst_1 34: goto 38 37: iconst_0 38: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V 41: return }
从 Code 3、6 可以看出字符串都被编译器优化成了字符串“abc”了。
总结
本文我们通过 javap -v XXX 的方式查看编译的代码发现 new String 首次会在字符串常量池中创建此字符串,那也就是说,通过 new 创建字符串的方式可能会创建 1 个或 2 个对象,如果常量池中已经存在此字符串只会在堆上创建一个变量,并指向字符串常量池中的值,如果字符串常量池中没有相关的字符,会先创建字符串在返回此字符串的引用给堆空间的变量。我们还将了字符串常量池在 JDK 1.7 和 JDK 1.8 的变化以及编译器对确定字符串的优化,希望能帮你正在的理解字符串的比较。
【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.com/live立即加入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK
原文发布时间:2020-04-17
本文作者:Java中文社群
本文来自:“掘金”,了解相关信息可以关注“掘金”
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
好程序员-JavaScript学习笔记-- GULP
JavaScript学习笔记-- GULPGULPgulp是一个项目开发的自动化打包构建工具基于node环境来运行的什么是自动化打包构建工具比如我们在开发的过程中,会写到js文件,css文件,等等我们的项目如果想上线,那么一定要体积小一点,文件大小越小越好而我们在写js文件的时候,会有很多换行/空格之类的东西这些换行/空格都是占文件体积的一部分那么我们在上线之前就要吧这些换行/空格尽可能的删除掉我们又不能一个文件一个文件的去删除就要用到一个自动化工具来帮助我们把这些多余的东西干掉这个就是自动化工具的意义常见的自动化打包构建工具gulpwebpack安装 GULPgulp是一个依赖于node的环境工具所以我们需要先安装一个全局gulp依赖直接使用npm去安装就可以了 使用 npm 安装全局依赖 gulp 我们这里安装一个 3.9.1 版本的就好了 $ npm install --global gulp@3.9.1等待安装完毕就好了这个全局环境一个电脑安装一次就好了还是照例检查一下是否安装成功$ gulp --version使用 GULP安装完毕以...
- 下一篇
学完Python好不好就业
Python的就业市场非常健康,你可能对此并不感到惊讶。“Python开发者”不仅是我们数据库中最受欢迎的职位之一,而且从历史上来说也是最稳定的职位之一。如果你想选择一种语言来入门编程,那么Python绝对是首选!其非常接近自然语言,精简了很多不必要的分号和括号,非常容易阅读理解。编程简单直接,更适合初学编程者,让其专注于编程逻辑,而不是困惑于晦涩的语法细节上,比起JAVA、C#和C/C++这些编程语言相对容易很多。即使是非计算机专业或者没有基础的小白,也能分分钟入门。Python的设计哲学是“优雅”、“明确”、“简单”,也因此决定了它是最文艺的编程语言。所以,也极力推荐妹子来学Python。语法清楚,干净,易读、易维护,代码量少,简短可读性强,团队协作开发时读别人的代码速度会非常快,更高效。通俗来说:“写起来快、看起来明白!”IT行业是一个需要不断自我挑战的行业,这就让很多人都想要进行尝试、挑战。IT行业的工作属于脑力劳动,需要不断攻克难关,且在工作的过程需要不断更新自己的技能知识,跟上时代的脚步。在该行业,从业者能够不断突破自己,一步步得到自我提升。参加北京Python编程培训好吗...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS8编译安装MySQL8.0.19
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7