老套的笔试题

在一些老套的笔试题中,会要你判断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对象使用 ...

随机推荐

  1. [日常摸鱼]bzoj3083遥远的国度-树链剖分

    一无聊就找树剖写 题意:一颗带点权的树,三种操作:1.换根 2.链赋值 3.查询子树最小值 如果没有换根的话直接就是裸的树剖了,对于换根的操作我们可以分类讨论. 1.如果查询的$x$就是根,那答案就是 ...

  2. python监控文件实时批量压缩脚本

    # coding:utf-8 from shutil import make_archive import os import time # 指定需要监测的文件夹 image_path = './im ...

  3. pytorch实现 | Deformable Convolutional Networks | CVPR | 2017

    文章转载自微信公众号:[机器学习炼丹术],请支持原创. 这一篇文章,来讲解一下可变卷积的代码实现逻辑和可视化效果.全部基于python,没有C++.大部分代码来自:https://github.com ...

  4. 第四章 Sentinel--服务容错

    我们接着承接上篇继续讲下去 : 第三章 Nacos Discovery–服务治理,开始第四篇的学习 第四章 Sentinel–服务容错 4.1 高并发带来的问题 在微服务架构中,我们将业务拆分成一个个 ...

  5. Azure Service Bus(一)入门简介

    一,引言 今天开始学习新的内容 Azure Service Bus(服务总线),其实也叫 "云消息服务",和 RabbitMQ,KafKa的一样都是作为消息通信服务,但是它们直接还 ...

  6. 如何优雅地使用云原生 Prometheus 监控集群

    作者陈凯烨,腾讯云前端开发工程师.负责 TKE 集群,弹性集群和云原生监控等模块控制台开发. 概述 Prometheus 是一套开源的系统监控报警框架.2016 年,Prometheus 正式加入 C ...

  7. 安装篇九:安装wordpress(5.4版本)

    #1.下载wordpress程序 下载部署wordpress博客程序(https://cn.wordpress.org/ 英文官网:https://www.wordpress.org/ ) [root ...

  8. [.NET] - 基础知识 - .NET Overview

    .NET Framework是有一个Framework Class Libray(FCL)和一个Common Language Runtim环境构成的,它 提供一个一致的面向对象的编程环境,而无论对象 ...

  9. VS编译程序添加图标

    如图 然后编译就就生成有图标的了.

  10. Log4j日志的级别

    log4j规定了默认的几个级别:ALL < trace < debug < info < warn < error < fatal  < OFF 1)级别之间 ...