当前位置: 首页 > 科技观察

灵魂拷问:Java的substring()是如何工作的?

时间:2023-03-17 01:00:09 科技观察

在浏览programcreek时,我发现了一些小而整洁的主题。例如:Java的substring()方法是如何工作的?像这样一个深思熟虑的话题非常值得深入研究。另外,我想告诉大家的是,研究的过程非常有趣,就像在迷宫中寻找宝藏一样。一开始有点不知所措,但经过一番细心探索,不仅会找到宝藏,还会有一种豁然开朗的感觉,非常好。对于绝大多数不太注重“内功”的初级程序员或老手来说,他们往往停留在“知其然不知其所以然”的层面——他们可以用,但如果想谈底层原理,他们只能用。我可以挠头,用手摊开一个问号脸。我也一直处于这个水平很长一段时间。但我决定改变,因为“内功”就像打基础。只有地基打好了,才能盖起经得起考验的高楼。借此机会,我将与大家一起深入学习《Java的substring()是如何工作的》。注意,准备打怪升级!1.什么是substring()?sub是subtract的缩写,所以substring的字面意思是“减去一个字符串”。经过这样的分析,是不是觉得方法的命名还是比较讲究的呢?substring()的完整写法是substring(intbeginIndex,intendIndex)。此方法返回原始字符串的起始索引beginIndex和结束索引endIndex-1之间的新字符串。Stringcmower="沉默王二,一个有趣的程序员";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)返回的字符串为“SilentKingTwo”——包括起始下标但不包括结束下标。2.调用substring()时发生了什么?在此之前,我们了解到字符串是不可变的,所以当调用substring()时,返回的其实是一个新的字符串。那么变量cmower的地址引用就会发生变化,如下图所示。为了证明上图完全正确,我们来看一下JDK7中substring()的源码publicString(charvalue[],intoffset,intcount){//checkboundarythis.value=Arrays.copyOfRange(value,offset),offset+count);}publicStringsubstring(intbeginIndex,intendIndex){//checkboundaryintsubLen=endIndex-beginIndex;returnnewString(value,beginIndex,subLen);}可以看出substring()通过newString()返回了一个新的字符串对象,并且在创建新对象时通过Arrays.copyOfRange()复制一个新的字符数组。但JDK6不同。说到JDK6,有些读者可能会表示不满。JDK6?现在是哪一年?JDK13出来了,好吗?但是我要告诉大家的是,对比分析JDK的源码,对于学习是大有裨益的。不是有句话说,要想了解一个成功的人,不仅要关注他发财之后做了什么,还要关注他发财之前做了什么。跟着小编一起看看JDK6中substring()的源码吧//JDK6String(intoffset,intcount,charvalue[]){this.value=value;this.offset=offset;this.count=count;}publicStringsubstring(intbeginIndex,intendIndex){//checkboundaryreturnnewString(offset+beginIndex,endIndex-beginIndex,value);}substring()方法本身和JDK7区别不大,都是通过newString()返回一个新的字符串对象。但是String()构造函数是完全不同的。JDK6只是简单的改变了两个属性(offset和count)的值,但是value并没有改变。PS:value是实际存放字符的数组,offset是数组第一个元素的下标,count是数组的字符个数。这是什么意思?虽然在调用substring()时创建了一个新的字符串,但是字符串的值仍然指向内存中的同一个数组,如下图所示。3、为什么JDK7的构造函数变了在看了JDK6和JDK7的源码之后,你可能会有这样的疑问:为什么JDK7需要变呢?大家共享同一个字符串数组不是很好吗?它节省了占用新的内存空间。实际上?如果有一个很长的字符串可以绕地球一圈,当我们需要调用substring()截取一个很小的字符串时,可能会造成性能问题。因为这个小字符串引用了整个长长字符数组,长长字符数组无法回收,内存一直被占用,可能会造成内存泄漏。PS:内存泄漏是指程序由于疏忽或错误而未能释放不再使用的内存。在JDK7出现之前,如何应对这个隐患呢?答案如下。cmower=cmower.substring(0,4)+"";为什么,为什么,为什么,多一个“+”“”就可以解决内存泄露的问题?可能有的读者不相信,那我就给大家分析一下。首先,我们通过JAD反编译字节码,上面这行代码就变成了下面这样。cmower=(newStringBuilder(String.valueOf(cmower.substring(0,4)))).toString();“+”运算符相当于语法糖。添加空字符串后,JDK会将其转换为StringBuilder对象,该对象在处理字符串时会生成一个新的字符数组,所以cmower=cmower.substring(0,4)+"";这行代码执行后,cmower指向的内容与substring()调用字符数组之前有所不同。PS:如果不明白“+”运算符的工作原理,请参考我之前的文章《羞,Java 字符串拼接竟然有这么多姿势》,这里不再赘述,免得被老读者打脸。4.最后总结一下,JDK7和JDK6的substring()方法本身没有太大变化,只是String类的构造函数有很大的不同。JDK7会重新拷贝一个字符数组,而JDK6不会,所以JDK6在执行比较长的字符串substring()时可能会造成内存泄漏。