别再问我 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 个多小时的时间,写作是一件很酷,并且能帮助他人的事,作者希望一直能坚持下去。如果觉得有用,请随手点击一个赞吧,谢谢

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546216.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#使用Sockets操作FTP【转载】

using System; using System.Collections; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions;/* *解析drwxr-xr-x *第一位表示文件类型。d是目录文件&#xff0c;l是链接文件&#xff0c;-是普通文件&a…

scala 字符串占位符_如何在Scala中将带有换行符的字符串转换为字符串列表?

scala 字符串占位符A string is a sequence of characters and it can contain multiple lines, for this, the string uses the newline character \n. And, we can separate each newline into a string and store them as a list of strings. 字符串是字符序列&#xff0c;…

一款开源免费的SSH/SFTP客户端Electerm,同时支持Linux、MacOS、Windows

简介&#xff1a; Electerm是一个跨平台的Terminal/SSH/SFTP客户端工具&#xff0c;同时支持Linux、MacOS、Windows&#xff0c;基于electron/ssh2/node-pty/xterm/antd/useProxy等开源组件。 下载地址&#xff1a; 官网下载&#xff1a;https://electerm.html5beta.com/ Git…

用了自定义Banner后,SpringBoot瞬间变的高大上了...

Spring Boot 在启动的时候&#xff0c;我们或许想要把自己公司的 logo&#xff0c;或者是项目的 logo 放上去&#xff0c;我们可以试试本文的这些方法&#xff0c;可以让你快速制作一些 Spring Boot 项目启动时的彩蛋&#xff0c;以提高项目的辨识度&#xff0c;或者是纯碎为了…

菜鸟学前端--javascript基础

在学习过css相关的知识&#xff0c;有了前端工程师的一些基础知识。但要较好的掌握前端&#xff0c;必须要学习好javascript的知识。下面将从基本语法、变量、关键字、保留字、语句、函数、BOM等角度阐释。一、基本语法javacript作为一种面向对象的、脚本级的轻量语言&#xff…

Java LinkedList void add(int index,Object o)方法,带示例

LinkedList void add(int index&#xff0c;Object o)方法 (LinkedList void add(int index, Object o) method) This method is available in package java.util.Collection and here, Collection is an interface. 该方法在java.util.Collection包中可用&#xff0c;在这里&a…

如何生成高性能的短链接?

前言今天&#xff0c;我们来谈谈如何设计一个高性能短链系统&#xff0c;短链系统设计看起来很简单&#xff0c;但每个点都能展开很多知识点&#xff0c;也是在面试中非常适合考察侯选人的一道设计题&#xff0c;本文将会结合我们生产上稳定运行两年之久的高性能短链系统给大家…

iOS 技术官方 QA

2019独角兽企业重金招聘Python工程师标准>>> Q: 在静态库中使用catagory分类运行时提示"selector not recognized" A: 需要配置下project/target属性 Q: 在iOS7以后怎么截图 A: iOS7 提供了相关API实现截图功能&#xff0c;如:-drawViewHierarchyInRect:a…

Python日历模块| weekheader()方法与示例

Python calendar.weekheader()方法 (Python calendar.weekheader() Method) weekheader() method is an inbuilt method of the calendar module in Python. It works on simple text calendars and returns a header containing the abbreviated names of the weekday. weekhe…

IPsec IKEv2(HCIP)

目录 一、IKE介绍 1、IKE介绍 2、IKE的主要作用 3、IKE与IPsec关系 二、IKE基础内容 1、IEK的身份认证方法 数据源认证 预共享密钥PSK 数字证书 数字信封 EAP(IKEv2支持) 数字证书CA如何实现身份认证? 2、IKEv1介绍 IKEv1介绍 IKEv1第一阶段介绍 IKEv1第二阶段…

9个小技巧让你的 if else看起来更优雅

if else 是我们写代码时&#xff0c;使用频率最高的关键词之一&#xff0c;然而有时过多的 if else 会让我们感到脑壳疼&#xff0c;例如下面这个伪代码&#xff1a; 是不是很奔溃&#xff1f;虽然他是伪代码&#xff0c;并且看起来也很夸张&#xff0c;但在现实中&#xff0c;…

poj 3254 状压dp

E -Corn FieldsTime Limit:2000MS Memory Limit:65536KB 64bit IO Format:%I64d & %I64u SubmitStatus Practice POJ 3254Description Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels…

第一弹!安利10个让你爽到爆的IDEA必备插件!

大家好&#xff0c;此篇文章中我会介绍10个非常不错的IDEA插件以及它们常见功能的使用方法。这一期内容搞 Gif 动态图花了很久&#xff0c;很多Gif图片上传到微信还提示过大&#xff0c;所以很多地方重新又录制了一遍Gif图。概览&#xff1a;IDE Features Trainer—IDEA交互式教…

C程序对整数中设置为1的位数进行计数

Problem statement: Write a C program to count number of bits set to 1 in an Integer. 问题陈述&#xff1a;编写一个C程序来计算Integer中设置为1的位数 。 Solution: We can use bitwise operator here to solve the problem. 解决方案&#xff1a;我们可以在这里使用按…

String性能提升10倍的几个方法!(源码+原理分析)

这是我的第 54 篇原创文章。String 类型是我们使用最频繁的数据类型&#xff0c;没有之一。那么提高 String 的运行效率&#xff0c;无疑是提升程序性能的最佳手段。我们本文将从 String 的源码入手&#xff0c;一步步带你实现字符串优化的小目标。不但教你如何有效的使用字符串…

日期getTime()方法以及JavaScript中的示例

JavaScript Date getTime()方法 (JavaScript Date getTime() method) getTime() method is a Dates class method and it is used to get the time in milliseconds from 1st January 1970. getTime()方法是Date的类方法&#xff0c;用于获取从1970年1月1 日开始的时间(以毫秒为…

制作openstack-centos镜像

一、准备工作我在计算节点上面制作镜像&#xff0c;计算节点为centos6.3 64位系统1.安装底层支持包yum groupinstall Virtualization "Virtualization Client"yum install libvirt2.下载或从本地上传进去一个完整的系统镜像mkdir /openstack-p_w_picpathcd /openstac…

一文彻底搞懂Java中的值传递和引用传递!

关于Java中方法间的参数传递到底是怎样的、为什么很多人说Java只有值传递等问题&#xff0c;一直困惑着很多人&#xff0c;甚至我在面试的时候问过很多有丰富经验的开发者&#xff0c;他们也很难解释的很清楚。我很久也写过一篇文章&#xff0c;我当时认为我把这件事说清楚了&a…

g++默认参数_C ++默认参数| 查找输出程序| 套装1

g默认参数Program 1: 程序1&#xff1a; #include <iostream>using namespace std;int sum(int X, int Y 20, int Z 30){return (X Y Z);}int main(){int A 0, B 0;A sum(5, 10);B sum(10);cout << A << " " << B;return 0;}Output…

c语言指针灵活性管窥

最近看到mit的[urlhttp://pdos.csail.mit.edu/6.828/2010/]操作系统课程网站[/url],其[urlhttp://pdos.csail.mit.edu/6.828/2010/labs/lab1/]实验一[/url] 中练习四&#xff08;exercise 4&#xff09;中有一个关于指针使用的代码&#xff1a;#include <stdio.h>#includ…