老套的笔试题

在一些老套的笔试题中,会要你判断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类的结构:

image-20210122222326753

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

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

image-20210123134243855

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

15

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

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

14

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

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

image-20210123135550260

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

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

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

image-20210123140717179

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

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

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

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

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

image-20210123154722863

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

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

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

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

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

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

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

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

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

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

总结而言就是:

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

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

image-20210123171825252

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

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

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

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

image-20210123160353095

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

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

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

image-20210123155156910

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

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

image-20210123170204407

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

image-20210123222415172

字符串拼接

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

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

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

image-20210123161329482

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

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

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

image-20210123221900789

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

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

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

image-20210123164646528

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

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

image-20210123170038866

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

2abe2cc517a513e4ac5fcec0d3669b22

intern方法

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

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

以这段代码为例:

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

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

image-20210123174512400

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

image-20210123174805405

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关键字。我们先看一下这段代码的字节码:

image-20210123171346440
image-20210123171437557

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

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

image-20210123223456766

所以原题就相当于:

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

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

总结

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

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

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

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

写在最后(求关注)

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

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

image-20210123180803720

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

我是CoderW,一个程序员。

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

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

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

  1. Java判断Bigdecimal类型是否等于0的方法

    1.我之前用来判断Bigdecimal类型是否等于0的方法b.equals(BigDecimal.ZERO);用equals方法和BigDecimal.ZERO进行比较。2.上面方法存在的问题有一天,调用这个这句代码的时候,传入的确实是0,但却返回false查看源代码发现:Bigdecimal的eq......

  2. 全面解析java final关键字

    根据上下文环境,Java 的关键字 final 的含义有些微的不同,但通常它指的是“这是不能被改变的”。防止改变有两个原因:设计或效率。因为这两个原因相差很远,所以有可能误用关键字 final。以下几节讨论了可能使用 final 的三个地方:数据、方法和类。1)final 数据对于编译时常量这种情况......

  3. Java中Jackson快速入门

    Java生态圈中有很多处理JSON和XML格式化的类库,Jackson是其中比较著名的一个。虽然JDK自带了XML处理类库,但是相对来说比较低级,使用本文介绍的Jackson等高级类库处理起来会方便很多。引入类库由于Jackson相关类库按照功能分为几个相对独立的,所以需要同时引入多个类库,为了方便......

  4. Java RMI 实现一个简单的GFS(谷歌文件系统)——介绍篇

    本系列主要是使用Java RMI 实现一个简单的GFS(谷歌文件系统,google file system)。首先进行整体介绍,然后对背景进行描述以及对系统进行设计,同时实现系统,提供演示视频和源代码,望多多支持!本系列主要是使用Java RMI实现一个简单的GFS(谷歌文件系统,google fi......

  5. Java Lombok简介、使用、工作原理、优缺点

    简介官方介绍Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java. Never write another getter or ......

  6. 使用Groovy构建DSL

    DSL(Domain Specific Language)是针对某一领域,具有受限表达性的一种计算机程序设计语言。常用于聚焦指定的领域或问题,这就要求 DSL 具备强大的表现力,同时在使用起来要简单。由于其使用简单的特性,DSL 通常不会像 Java,C++等语言将其应用于一般性的编程任务。对于 G......

  7. JVM笔记 -- Java跨平台和JVM跨语言

    学习JVM的重要性从上层应用程序到底层操作系统,到底有哪些东西?平时开发的应用程序主要基于各种框架,譬如Spring,SpringMVC,Mybatis,而各种框架又是基于Java API来实现的,Java API调用执行是在JVM上的,而JVM则是运行在操作系统上的,操作系统是在物理机器打交道的。......

  8. 【JAVA并发第三篇】线程间通信

    线程间的通信JVM在运行时会将自己管理的内存区域,划分为不同的数据区,称为运行时数据区。每个线程都有自己私有的内存空间,如下图示:Java线程按照自己虚拟机栈中的方法代码一步一步的执行下去,在这一过程中不可避免的会使用到线程共享的内存区域堆或方法区。为了防止多个线程在同一时刻访问同一个内存地址,需要......

  9. Java中的CPU占用高和内存占用高的问题排查

    下面通过模拟实例分析排查Java应用程序CPU和内存占用过高的过程。如果是Java面试,这2个问题在面试过程中出现的概率很高,所以我打算在这里好好总结一下。1、Java CPU过高的问题排查举个例子,如下:package com.classloading;public class Test {sta......

  10. java中throws实例用法详解

    在程序出现异常时,会有一个抛出异常的throw出现,这里我们要跟今天所讲的throws区分开。throws的作用是声明抛出,在名称上也跟throw有所不同。下面我们就throws对策概念、语法、实例带来讲解,帮助大家找到声明抛出异常的方法,具体方法如下。1.概念如果方法声明的是Exception类型......

随机推荐

  1. 详解MySQL与Spring的自动提交(autocommit)

    1 MySQL的autocommit设置MySQL默认是开启自动提交的,即每一条DML(增删改)语句都会被作为一个单独的事务进行隐式提交。如果修改为关闭状态,则执行DML语句之后要手动提交 才能生效。查询当前会话的自动提交是否开启:mysql> show variables like 'aut......

  2. Python中OS对目录的操作以及引用

    路径的获取对当前目录的获取1 path = os.getcwd() 2 print("获取到的当前目录是:({})".format(path))获取当前文件所在的绝对路径import ospath = os.path.realpath(__file__) print("......

  3. python pillow库的基础使用教程

    知识点图像模块 (Image.Image)Image模块的功能Image模块的方法ImageChops模块ImageColor模块基础使用图像模块 Image.Image加载图像对象,旋转90度并显示from PIL import Image#显示图像im = Image.open('backgro......

  4. 详解Python之Scrapy爬虫教程NBA球员数据存放到Mysql数据库

    获取要爬取的URL爬虫前期工作用Pycharm打开项目开始写爬虫文件字段文件items# Define here the models for your scraped items## See documentation in:# https://docs.scrapy.org/en/latest/......

  5. MySQL全面瓦解17:触发器相关

    关于触发器现实开发中我们经常会遇到这种情况,比如添加、删除和修改信息的时候需要记录日志,我们就要在完成常规的数据库逻辑操作之后再去写入日志表,这样变成了两步操作,更复杂了。又比如删除一个人员信息的时候,需要将他的购物记录、收货地址、收藏夹等都删了,这个连续的操作容易出错,一致性和完整性不好保证。这时......

  6. java自定义注解验证手机格式的实现示例

    1、@Valid与@Validated的区别1.1 基本区别@Valid:Hibernate validation校验机制@Validated:Spring Validator校验机制,这个也是最常用的@Validation只是对@Valid进行了二次封装,在使用上并没有太大区别,但在分组、注解位置......

  7. Linux磁盘空间释放问题整理

    IDC里的一台服务器的/分区使用率爆满了!已达到100%!经查看发现有个文件过大(80G),于是在跟有关同事确认后rm -f果断删除该文件。但是发现删除该文件后,/分区的磁盘空间压根没有释放出来,使用率还是100%!这是为什么呢??[root@linux-node1 ~]# df -hFilesys......

  8. PHP设计模式之原型模式示例详解

    前言原型模式其实更形象的来说应该叫克隆模式。它主要的行为是对对象进行克隆,但是又把被克隆的对象称之为最初的原型,于是,这个模式就这样被命名了。说真的,从使用方式来看真的感觉叫克隆模式更贴切一些。Gof类图及解释GoF定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象GoF类图代码实......

  9. Python优化机制:常量折叠

    英文:https://arpitbhayani.me/blogs/constant-folding-python作者:arprit译者:豌豆花下猫(“Python猫”公众号作者)声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。每种编程语言为......

  10. Java并发包源码学习系列:阻塞队列实现之LinkedBlockingQueue源码解析

    目录LinkedBlockingQueue概述类图结构及重要字段构造器出队和入队操作入队enqueue出队dequeue阻塞式操作E take() 阻塞式获取void put(E e) 阻塞式插入E poll(timeout, unit) 阻塞式超时获取boolean offer(e, timeou......