老套的笔试题

在一些老套的笔试题中,会要你判断s1==s2为false还是true,s1.equals(s2)为false还是true。

String s1 = new String("xyz");
String s2 = "xyz";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));

对于这种题,你总能很快的给出标准答案:==比较的是对象地址,equals方法比较的是真正的字符数组。所以输出的是false和true。

上面的属于最低阶的题目,没有什么难度。

现在这种老套的题目已经慢慢消失了,取而代之的是有一些变形的新题目:

String s1 = "aa";
String s2 = "bb";
String str1 = s1 + s2;
String str2 = "aabb";
//输出什么呢???
System.out.println(str1 == str2); final String s3 = "cc";
final String s4 = "dd";
String str3 = s3 + s4;
String str4 = "ccdd";
//又输出什么呢???
System.out.println(str3 == str4);

难度提升了一些,但思考一下也不难得出答案是false和true。

今天的文章就是以这几个题目展开的。

String对象的创建

先简单看一下String类的结构:

可以发现,String里面有一个value属性,是真正存储字符的char数组。

在执行String s = "xyz";的时候,在堆区创建了一个String对象,一个char数组对象。

如何证明创建了一个String对象和一个char数组对象呢?我们可以通过IDEA的Debug功能验证:

注意看我截图的位置,在执行完String s = "xyz";之后,再次点击load classes,Diff栏的String和char[]分别加了1,表示在内存中新增了一个char数组对象和一个String对象。

现在,我们再来看String s = new String("xyz");创建了几个对象。

从这张Debug动图中,我们可以得出在String s = new String("xyz");之后,创建了两个String对象和一个char数组对象。

又因为String s = new String("xyz");s引用只能指向一个对象,可以画出内存分布图:

从图中可以看到,在堆区,有两个String对象,这两个String对象的value都指向同一个char数组对象。

那么问题来了,下面的那个String对象根本就没被引用,也就是说他没有被用到,那么它到底是干什么的呢?

占了内存空间又不使用,难道这是JDK的设计缺陷?

很显然不是JDK的缺陷,JDK虽然确实有设计缺陷,但不至于这么明显,这么愚蠢。

那下面的那个String对象是干什么的呢?

答案是用于驻留到字符串常量池中去的,注意,这里我用了一个驻留,并不是直接把对象放到字符串常量池里面去,有什么区别我们后面再讲。

这里出现了字符串常量池的概念,我在String s = new String("xyz")创建了几个实例你真的能答对吗?中也有过比较详细的介绍,有兴趣的可以去看一下,这里不再重复了。

你只需要知道,字符串常量池在JVM源码中对应的类是StringTable,底层实现是一个Hashtable。

我们以String s = new String("xyz");为例:

首先去找字符串常量池找,看能不能找到“xyz”字符串对应对象的引用,如果字符串常量池中找不到:

  • 创建一个String对象和char数组对象
  • 将创建的String对象封装成HashtableEntry,作为StringTable的value进行存储
  • new String("xyz")会在堆区又创建一个String对象,char数组直接指向创建好的char数组对象

如果字符串常量池中能找到:

  • new String("xyz")会在堆区创建一个对象,char数组直接指向已经存在的char数组对象

String s = "xyz";是怎么样的逻辑:

首先去找字符串常量池找,看能不能找到“xyz”字符串的引用,如果字符串常量池中能找不到:

  • 创建一个String对象和char数组对象
  • 将创建的String对象封装成HashtableEntry,作为StringTable的value进行存储
  • 返回创建的String对象

如果字符串常量池中能找到:

  • 直接返回找到引用对应的String对象

总结而言就是:

对于String s = new String("xyz");这种形式创建字符串对象,如果字符串常量池中能找到,创建一个String对象;如果如果字符串常量池中找不到,创建两个String对象。

对于String s = "xyz";这种形式创建字符串对象,如果字符串常量池中能找到,不会创建String对象;如果如果字符串常量池中找不到,创建一个String对象。

所以,在日常开发中,能用String s = "xyz";尽量不用String s = new String("xyz");,因为可以少创建一个对象,节省一部分空间。

需要强调的是,字符串常量池存的不是字符串也不是String对象,而是一个个HashtableEntry,HashtableEntry里面的value指向的才是String对象,为了不让表述变得复杂,我省略了HashtableEntry的存在,但不代表它就不存在。

上文提到的驻留就是新建HashtableEntry指向String对象,并把HashtableEntry存入字符串常量池的过程。

在网上一些文章中,一些作者可能是为了让读者更好的理解,省略了一些这些,一定要注意辨别区分。

达成以上共识之后,我们再回顾一下那个老套的笔试题。

String s1 = new String("xyz");
String s2 = "xyz";
//为什么输出的是false呢?
System.out.println(s1 == s2);
//为什么输出的是true呢?
System.out.println(s1.equals(s2));

有了上面的基础之后,我们画出对应的内存图,s1 == s2为什么是false就一目了然了。

因为equals方法比较的真正的char数据,而s1和s2最终指向的都是同一个char数组对象,所以s1.equals(s2)等于true。

关于他们最终指向的都是同一个char数组对象这一观点,也可以通过反射证明:

我修改了str1指向的String对象的value,str2指向的对象也被影响了。

字符串拼接

现在,我们再来看一下变式题:

String s1 = "aa";
String s2 = "bb";
String str1 = s1 + s2;
String str2 = "aabb";
//为什么输出的是false
System.out.println(str1 == str2);

对于这个题目,我们需要先看一下这段代码的字节码。

字节码指令看不懂没有关系,看我用红色框框起来的部分就行了,可以看到居然出现了StringBuilder。

什么意思呢,就是说String str1 = s1 + s2;会被编译器会优化成new StringBuilder().append("aa").append("bb").toString();

StringBuilder里面的append方法就是对char数组进行操作,那StringBuilder的toString方法做了什么呢?

从源码中可以看到,StringBuilder里面的toString方法调用的是String类里面的String(char value[], int offset, int count)构造方法,这个方法做了什么呢?

  • 根据参数复制一份char数组对象。复制了一份!
  • 创建一个String对象,String对象的value指向复制的char数组对象。

注意,并没有驻留到字符串常量池里面去,这个很关键!!!画一个图理解一下:

也就是说str2指向的String对象并没有驻留到字符串常量池,而str1指向的对象驻留到字符串常量池里面去了,且他们并不是同一个对象。所以str1 == str2还是false

因为复制一份char数组对象,所以如果我们改变其中一个char数组的话,另一个也不会造成影响:

把其中String变成丑比之后,另一个还是帅比,也说明了两个String对象用的不是同一份char数组。

intern方法

上面说到,调用StringBuilder的toString方法创建的String对象是不会驻留到字符串常量池的,那如果我偏要驻留到字符串常量池呢?有没有办法呢?

有的,String类的intern方法就可以帮你完成这个事情。

以这段代码为例:

String s1 = "aa";
String s2 = "bb";
String str = s1 + s2;
str.intern();

在执行str.intern();之前,内存图是这样的:

在执行str.intern();之后,内存图是这样的:

intern方法就是创建了一个HashtableEntry对象,并把value指向String对象,然后把HashtableEntry通过hash定位存到对应的字符串成常量池中。当然,前提是字符串常量池中原来没有对应的HashtableEntry。

没了,intern方法,就是这么简单,一句话给你说清楚了。

关于intern方法,还有一个很有趣的故事,有兴趣的可以去看一下why神的这篇文章《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了

编译优化

写到这里,好像只有一个坑没有填。就是这个题为什么输出的是true。

final String s3 = "cc";
final String s4 = "dd";
String str3 = s3 + s4;
String str4 = "ccdd";
//为什么输出的是true呢???
System.out.println(str3 == str4);

这道题和上面那道题相比,有点相似,在原来的基础上加了两个final关键字。我们先看一下这段代码的字节码:

又是一段字节码指令,不需要看懂,你点一下#4,居然就可以看到“ccdd”字符串。

原来,用final修饰后,JDK的编译器会识别优化,会把String str3 = s3 + s4;优化成String str3 = "ccdd"

所以原题就相当于:

String str3 = "ccdd";
String str4 = "ccdd";
//为什么输出的是true呢???
System.out.println(str3 == str4);

这样的题目还难吗?是不是那不管str3和str4怎么比,肯定是相等的。

总结

String对于Java程序员来说就是“最熟悉的陌生人”,你说String简单,它确实简单。你说它难,深究起来确实也有难度,但这些题目,只要你脑海里有一副内存图就会很简单。

面试题也只会越来越难,这个行业看起来也越来越内卷,但只要我学的快,内卷就卷不到我。

好了,今天就写到了,我要去打游戏了。

希望这篇文章,能对你有一点帮助。

写在最后(求关注)

我对每一篇发出去的文章负责,文中涉及知识理论,我都会尽量在官方文档和权威书籍找到并加以验证。但即使这样,我也不能保证文章中每个点都是正确的,如果你发现错误之处,欢迎指出,我会对其修正。

创作不易,为了更好的表达,需要画很多图,这些都是我自己动手用PPT画的,画图也很辛苦的!

所以,不要犹豫了,给点正反馈,答应我,一键三连(关注、点赞、再看)好吗?

我是CoderW,一个程序员。

谢谢你的阅读,我们下期再见!

更多精彩关注微信公众号【CoderW】

Java中,那些关于String和字符串常量池你不得不知道的东西的更多相关文章

  1. java中的堆、栈、常量池

    java中的堆.栈.常量池 分类: java2010-01-15 03:03 4248人阅读 评论(5) 收藏 举报 javastring编译器jvm存储equals Java内存分配: 1. 寄存器 ...

  2. String:字符串常量池

    String:字符串常量池 作为最基础的引用数据类型,Java 设计者为 String 提供了字符串常量池以提高其性能,那么字符串常量池的具体原理是什么,我们带着以下三个问题,去理解字符串常量池: 字 ...

  3. 详细介绍Java中的堆、栈和常量池

    下面主要介绍JAVA中的堆.栈和常量池: 1.寄存器 最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制. 2. 栈 存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在 ...

  4. java中的堆、栈、常量池以及String类型的两种声明

    参考自http://blog.sina.com.cn/s/blog_798b04f90100ta67.html http://www.cnblogs.com/fguozhu/articles/2661 ...

  5. Java String:字符串常量池(转)

    作为最基础的引用数据类型,Java 设计者为 String 提供了字符串常量池以提高其性能,那么字符串常量池的具体原理是什么? 字符串常量池的设计思想是什么? 字符串常量池在哪里? 如何操作字符串常量 ...

  6. java中的堆、栈和常量池简介

    一.它们各自存放的数据类型: 堆:存放所有new出来的对象. 栈:存放基本类型的变量数据和对象的应用,对象(new出来的对象)本身并不存在栈中,而是存放在堆中或者常量池中(字符串常量对象存放在常量池中 ...

  7. 在java中,将String类型字符串s赋值为null后,将字符串与其他字符串拼接后得到结果出现了null字符串与其他字符连接的样式

    String s = null; s  += "hello"; System.out.println(s); 结果为:nullhello 原因: 先应用String.valueOf ...

  8. Java中String字符串常量池总结

    最近到广州某建站互联网公司面试,当时面试官问假设有两个字符串String a="abc",String b = "abc";问输出a==b是true还是fals ...

  9. java字符串常量池——字符串==比较的一个误区

    转自:https://blog.csdn.net/wxz980927155/article/details/81712342   起因 再一次js的json对象的比较中,发现相同内容的json对象使用 ...

  10. String字符串常量池简介

    直接贴代码---> public class Test { public static void main(String[] args) { /** * 为了提升字符串的访问效率,在程序中使用了 ...

随机推荐

  1. 深搜+DP剪枝 codevs 1047 邮票面值设计

    codevs 1047 邮票面值设计 1999年NOIP全国联赛提高组  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 钻石 Diamond 题目描述 Description ...

  2. easyui添加自定义验证规则

    $.extend($.fn.validatebox.defaults.rules, { phone: { validator: function (value) { return /^(\d{3,4} ...

  3. MFC简易画图

    开发一个MFC绘图程序,基于"文档-视图"结构,在客户区能够完成: ①画直线 ②画矩形 ③画圆(椭圆) ④画任意折线(右键结束) ⑤画任意多边形(右键结束,并形成闭环) 注:图形中 ...

  4. 获取input中的值

    先在input标签中添加一个id='getvalue'document.getElementById("getvalue").value;

  5. C#对文件的操作

    本文收集了目前最为常用的C#经典操作文件的方法,具体内容如下:C#追加.拷贝.删除.移动文件.创建目录.递归删除文件夹及文件.指定文件夹下 面的所有内容copy到目标文件夹下面.指定文件夹下面的所有内 ...

  6. .Net高并发解决思路(附源码)

    本文如有不对之处,欢迎各位拍砖扶正.另源码在文章最下面,大家下载过后先还原一下nuget包,需要改一下redis的配置,rabbitmq的配置以及Ef的连接字符串.另外使用的是CodeFirst,先u ...

  7. Codeforces1101F Trucks and Cities 【滑动窗口】【区间DP】

    题目分析: 2500的题目为什么我想了这么久... 考虑答案是什么.对于一辆从$s$到$t$的车,它有$k$次加油的机会.可以发现实际上是将$s$到$t$的路径以城市为端点最多划分为最大长度最小的$k ...

  8. Android学习之BitMap用法实例

    下面简单说明了BitMap的用法: 从服务器下载一张图片,显示在ImageView控件上,并将该图片保存在移动设备的SD上. // 根据网络URL获取输入流 public InputStream ge ...

  9. 让pip 使用国内镜像源

    让python的pip使用 国内镜像 国内源: 清华:https://pypi.tuna.tsinghua.edu.cn/simple 阿里云:http://mirrors.aliyun.com/py ...

  10. Mybatis插入数据返回自增主键

    方法有很多,参考 mysql函数之六:mysql插入数据后返回自增ID的方法,last_insert_id(),selectkey 这里记录一下工作中自己用到的selectkey方法的详细过程. po ...