在逛 programcreek 的时候,我发现了一些小而精悍的主题。比如说:Java 的 substring() 方法是如何工作的?像这类灵魂拷问的主题,非常值得深入地研究一下。

另外,我想要告诉大家的是,研究的过程非常的有趣,就好像在迷宫里探宝一样,起初有些不知所措,但经过一番用心的摸索后,不但会找到宝藏,还会有一种茅塞顿开的感觉,非常棒。

对于绝大多数的初级程序员或者说不重视“内功”的老鸟来说,往往停留在“知其然不知其所以然”的层面上——会用,但要说底层的原理,可就只能挠挠头双手一摊一张问号脸了。

很长一段时间内,也一直处于这种层面上。但我决定改变了,因为“内功”就好像是在打地基,只有把地基打好了,才能盖起经得住考验的高楼大厦。借此机会,我就和大家一起,对“Java 的 substring() 是如何工作的”进行一次深入地研究。注意了,准备打怪升级了!

01、substring() 是干嘛的

subsubtract 的缩写,因此 substring 的字面意思就是“把字符串做个减法”。这样一分析,是不是感觉方法的命名还是蛮有讲究的?

substring() 的完整写法是 substring(int beginIndex, int endIndex)。该方法返回一个新的字符串,介于原有字符串的起始下标 beginIndex 和结尾下标 endIndex-1 之间。

String cmower = "沉默王二,一枚有趣的程序员";
cmower = cmower.substring(0, 4);
System.out.println(cmower);

程序输出的结果为:

沉默王二

为什么呢?我来简单解释一下。

Java 的下标都是从 0 开始编号的(我不确定有没有从 1 开始的编程语言),这和我们平常生活中从 1 开始编号的习惯不同。Java 这样做的原因如下:

Java 是基于 C 语言实现的,而 C 语言的下标是从 0 开始的——这听起来好像是一句废话。真正的原因是下标并不是下标,在指针(C)语言中,它实际上是一个偏移量,距离开始位置的一个偏移量。第一个元素在开头,因此它的偏移量就为 0。

此外,还有另外一种说法。早期的计算机资源比较匮乏,0 作为起始下标相比较于 1 作为起始下标,编译的效率更高。

知道了这层原因后,再来看上面这段代码,就会豁然开朗。对于“沉默王二,一枚有趣的程序员”这串字符来说,“沉”的下标为 0,“默”的下标为 1,“王”的下标为 2,“二”的下标为 3,所以 cmower.substring(0, 4) 返回的字符串是“沉默王二”——包括起始下标但不包括结尾下标。

02、substring() 在被调用的时候究竟发生了什么?

在此之前,我们已经了解到:[字符串是不可变的](),因此当调用 substring() 方法的时候,返回的其实是一个新的字符串。那么变量 cmower 的地址引用就会发生如下图所示的变化。


为了证明上图是完全正确的,我们来看一下 JDK 7 中 substring() 的源码。

public String(char value[], int offset, int count) {
    //check boundary
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}

public String substring(int beginIndex, int endIndex) {
    //check boundary
    int subLen = endIndex - beginIndex;
    return new String(value, beginIndex, subLen);
}

可以看得出,substring() 通过 new String() 返回了一个新的字符串对象,在创建新的对象时通过 Arrays.copyOfRange() 复制了一个新的字符数组。

但 JDK 6 就有所不同。说到 JDK 6,可能有些读者表示不服,JDK 6?什么年代了,JDK 13 都出来了好不好?但我想告诉大家的是,对比着剖析 JDK 的源码,对学习大有裨益。

不是有那么一句话嘛,要想了解一个成功人士,不能只关注他发迹以后的事,更要关注他之前做了什么。

就请随我来,看看 JDK 6 中的 substring() 的源码吧。

//JDK 6
String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

public String substring(int beginIndex, int endIndex) {
    //check boundary
    return  new String(offset + beginIndex, endIndex - beginIndex, value);
}

substring() 方法本身和 JDK 7 并没有很大的差别,都通过 new String() 返回了一个新的字符串对象。但是 String() 这个构造函数有很大的差别,JDK 6 只是简单地更改了一下两个属性(offsetcount)的值,value 并没有变。

PS:value 是真正存储字符的数组,offset 是数组中第一个元素的下标,count 是数组中字符的个数。

这意味着什么呢?

调用 substring() 的时候虽然创建了新的字符串,但字符串的值仍然指向的是内存中的同一个数组,如下图所示。


03、为什么 JDK 7 的构造函数发生了变化

看了 JDK 6 和 JDK 7 源码之后,大家可能产生这样一个疑惑:为什么 JDK 7 要做出改变呢?大家共用同一个字符串数组不是挺好的嘛,省得占用新的内存空间。事实上呢?

如果有一个很长很长的字符串,可以绕地球一周,当我们需要调用 substring() 截取其中很小一段字符串时,就有可能导致性能问题。由于这一小段字符串引用了整个很长很长的字符数组,就导致很长很长的这个字符数组无法被回收,内存一直被占用着,就有可能引发内存泄露。

PS:内存泄露是指由于疏忽或错误造成程序未能释放已经不再使用的内存。

那 JDK 7 出现之前,这个隐患怎么应对呢?答案如下。

cmower = cmower.substring(0, 4) + "";

为什么,为什么,为什么,多一个 “+ ""” 就能解决内存泄漏的问题?有些读者可能不太相信,我来带大家分析一下。

首先呢,我们通过 JAD 对字节码反编译一下,上面这行代码就变成了如下内容。

 cmower = (new StringBuilder(String.valueOf(cmower.substring(0, 4)))).toString();

“+”号操作符就相当于一个语法糖,加上空的字符串后,会被 JDK 转化为 StringBuilder 对象,该对象在处理字符串的时候会生成新的字符数组,所以 cmower = cmower.substring(0, 4) + ""; 这行代码执行后,cmower 就指向了和 substring() 调用之前不同的字符数组。

PS:如果不明白“+”号操作符的工作原理,请查阅我之前写的文章《羞,Java 字符串拼接竟然有这么多姿势》,这里就不再赘述,免得被老读者捶。

04、最后

总结一下,JDK 7 和 JDK 6 的 substring() 方法本身并没有多大的改变,但 String 类的构造函数有了很大的区别,JDK 7 会重新复制一份字符数组,而 JDK 6 不会,因此 JDK 6 在执行比较长的字符串 substring() 时可能会引发内存泄露的问题。


好了各位读者朋友们,以上就是本文的全部内容了。能看到这里的都是最优秀的程序员,二哥必须要为你点个赞

灵魂拷问:Java 的 substring() 是如何工作的?的更多相关文章

  1. 工厂设计模式灵魂拷问-Java实现

    show me the code and take to me,做的出来更要说的明白 GitHub项目JavaHouse同步收录 喜欢就点个赞呗! 你的支持是我分享的动力! 引入 我们经常听到工厂模式 ...

  2. 灵魂拷问:为什么 Java 字符串是不可变的?

    在逛 programcreek 的时候,发现了一些精妙绝伦的主题.比如说:为什么 Java 字符串是不可变的?像这类灵魂拷问的主题,非常值得深思. 对于绝大多数的初级程序员来说,往往停留在" ...

  3. 灵魂拷问:如何检查Java数组中是否包含某个值 ?

    在逛 programcreek 的时候,我发现了一些专注细节但价值连城的主题.比如说:如何检查Java数组中是否包含某个值 ?像这类灵魂拷问的主题,非常值得深入地研究一下. 另外,我想要告诉大家的是, ...

  4. 灵魂拷问:创建 Java 字符串,用""还是构造函数

    在逛 programcreek 的时候,我发现了一些小而精悍的主题.比如说:创建 Java 字符串,用 "" 还是构造函数?像这类灵魂拷问的主题,非常值得深入地研究一下. 01.& ...

  5. 灵魂拷问:Java对象的内存分配过程是如何保证线程安全的?(阿里面试)

    JVM内存结构,是很重要的知识,相信每一个静心准备过面试的程序员都可以清楚的把堆.栈.方法区等介绍的比较清楚. 上图,是一张在作者根据<Java虚拟机规范(Java SE 8)>中描述的J ...

  6. 灵魂拷问:你真的理解System.out.println()执行原理吗?

    原创/朱季谦 灵魂拷问,这位独秀同学,你会这道题吗?  请说说,"System.out.println()"原理...... 这应该是刚开始学习Java时用到最多一段代码,迄今为止 ...

  7. java中substring和indexof() 和lastindexof()

    java中substring和indexof() 和lastindexof() str=str.substring(int beginIndex);截取掉str从首字母起长度为beginIndex的字 ...

  8. java中substring的使用方法

    java中substring的使用方法 str=str.substring(int beginIndex);截取掉str从首字母起长度为beginIndex的字符串,将剩余字符串赋值给str: str ...

  9. Java垃圾回收机制的工作原理

    Java垃圾回收机制的工作原理 [博主]高瑞林 [博客地址]http://www.cnblogs.com/grl214 获取更多内容,请关注小编个人微信公众平台: 一.Java中引入垃圾回收机制的作用 ...

随机推荐

  1. python学习之【第十一篇】:Python中的文件操作

    1.前言 在Python中,对文件的操作主要遵循以下流程: 打开文件,得到文件句柄并赋值给一个变量 通过文件句柄对文件进行操作 关闭文件 2.打开文件 使用open函数,可以打开一个已经存在的文件,或 ...

  2. git上传项目到github远程库

    最近在学习使用 git 上传管理项目,依照教程,建好了一个远程库,也实现了本地库与远程库的项目同步上传,但是在试着将本地库里的项目上传到另一个新建远程库时遇到了问题,一直上传不成功,经过一番查找摸索终 ...

  3. centos7上以RPM方式安装MySQL5.6

    1. 下载MySQL http://ftp.ntu.edu.tw/MySQL/Downloads/MySQL-5.6/ MySQL-5.6.36-1.el7.src.rpm MySQL-5.6.36- ...

  4. Mac中的Python安装selenium,结合chrom及chromdriver使用

    一.安装selenium 1.在终端通过命令安装 pip3 install -U selenium 二.准备环境 1.在电脑中安装谷歌浏览器chrom,和下载估计浏览器驱动chromdriver,以下 ...

  5. [转载]2.6 UiPath循环嵌套的介绍和使用

    一.循环嵌套的介绍 一个循环体内又包含另一个完整的循环结构,就称之为循环嵌套.内嵌的循环中还可以嵌套循环,这就是多层循环,也叫做多重循环. 二.在UiPath中结合使用循环嵌套生成99乘法表 1.打开 ...

  6. [springboot 开发单体web shop] 6. 商品分类和轮播广告展示

    商品分类&轮播广告 因最近又被困在了OSGI技术POC,更新进度有点慢,希望大家不要怪罪哦. 上节 我们实现了登录之后前端的展示,如: 接着,我们来实现左侧分类栏目的功能. ## 商品分类|P ...

  7. 手把手教你如何在阿里云ECS搭建Python TensorFlow Jupyter

    前段时间在阿里云买了一台服务器,准备部署网站,近期想玩一些深度学习项目,正好拿来用.TensorFlow官网的安装仅提及Ubuntu,但我的ECS操作系统是 CentOS 7.6 64位,搭建Pyth ...

  8. nyoj 599-奋斗的小蜗牛 (double ceil(); (temp - 1) / 5)

    599-奋斗的小蜗牛 内存限制:64MB 时间限制:1000ms 特判: No 通过数:0 提交数:96 难度:1 题目描述: 传说中能站在金字塔顶的只有两种动物,一种是鹰,一种是蜗牛.一只小蜗牛听了 ...

  9. .NET Core 3 WPF MVVM框架 Prism系列之数据绑定

    一.安装Prism 1.使用程序包管理控制台 Install-Package Prism.Unity -Version 7.2.0.1367 也可以去掉‘-Version 7.2.0.1367’获取最 ...

  10. Win32 COM组件 x Android Service

    有些书在介绍和讲解android的Service组件时,会使用后台服务一词,并且与运行在主线程的Activity相对.因为后台一词很容易误解,服务一直运行在后台?什么线程在运行?服务一直有条线程在运行 ...