Java字符串的截取操作可以通过substring来完成。有意思的是,这个方法从jdk1.0开始,一直到1.6都没有变化,但到了1.7实现方式却发生了改变。你可能会认为之所以要对一个成熟且稳定的方法做修改,一定是因为新的实现更好、效率更高吧?然而正好相反,修改后的substring的效率变低了,并且占用了更多的内存,无论是从时间上还是空间上都比不上原有的实现。下面我们来做一个比较,看看到底哪一个更好,以及为什么新版Java中要对其进行修改。

原有实现

我们首先来看看原来的substring方法。前面是对参数进行检查,重点是最后一句:

return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);

这里通过调用下面这个构造方法来创建一个新的字符串:

// Package private constructor which shares value array for speed.
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}

我们知道,Java的字符串实际上是用一个字符数组来实现的,这个构造方法通过复用字符数组value,省去了数组拷贝的开销,仅通过3个赋值语句就创建了一个新的字符串对象。从注释也可以看出这个构造方法的意图就是为了提升性能。

新的实现

我们再来看看1.7中新的substring实现。前面一堆还是参数检查,直接看最后一句:

return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);

与原来的差不多,但是请注意,这次调用的是另一个构造方法:

public String(char[], int, int)

这个公有的构造方法和前面那个很相似(那个是包私有的),从方法签名上看区别仅仅是参数顺序不同。不过这只是表面现象,它们的内部实现却是完全不同的,这个公有的构造方法不会复用char[]数组,而是将其拷贝到一个新数组,从而创建一个新字符串。

this.value = Arrays.copyOfRange(value, offset, offset+count);

对公有的构造方法来说,必须采用这种方式,如果仍然采用复用数组的方法,就会发生安全性问题,别人就可以对字符串中的字符进行任意的修改。后面会对此进行分析。

复用字符数组有没有安全隐患

Java的字符串是不可变的,原因是作为字符串底层实现的字符数组是私有的,从外面无法访问。另一方面,String类的每一个可以创建新字符串的公有方法(构造方法、valueOf等),如果其接受一个字符数组作为参数,就会对该数组执行拷贝操作,这就进一步保证了只有String对象才会持有它的字符数组,因此断绝了从外部修改数组的一切可能。

如果不这么做就会带来问题,字符串的不可变性也就不复存在了。比如下面这个假想的程序:

char[] arr = new char[] {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
String s = new String(0, arr.length, arr); // "hello world"
arr[0] = 'a'; // replace the first character with 'a'
System.out.println(s); // aello world

如果构造方法没有对arr进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对arr的修改就相当于修改了字符串。(可以通过反射来真正地实现这个假想的程序)

还有一些方法,比如原来的substring方法,它们没有进行数组拷贝,而是直接复用另一个字符串的内部数组。这样做会导致安全问题吗?答案是不会,因为所有这些方法所执行的操作都是私有操作或包私有操作,属于内部实现,因此只要不对外暴露这些操作的接口就仍然是安全的。

例如对substring来说,由于无论是原字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。

为何要修改substring

原来的substring在安全上没有问题,而且性能很好,又能共享内部数组节约内存。这么看来,好像并没有什么缺点。那为什么要放弃性能更好的实现方式,而采用性能差很多的数组拷贝的方式呢?难道是Oracle的工程师脑抽才会对substring做出这样的修改吗?

当然不是,原来的方法比新的好只是表面现象,因为虽然性能好,但是有一个严重的问题,那就是有可能会导致内存泄漏。看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。下面是示例代码。

String aLongString = ...; // a very long string
String aPart = data.substring(20, 40);
return aPart;

在这里aLongString只是临时的,真正有用的是aPart,其长度只有20个字符,但是它的内部数组却是从aLongString那里共享的,因此虽然aLongString本身可以被回收,但它的内部数组却不能(如下图)。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降。

新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的substring比原来的更健壮。

实际上前面所说的那个包私有的构造方法在1.7中已经被标记为Deprecated,并且实现也修改为直接调用“public String(char[], int, int)”。到了1.8这个构造方法就被删除了。取而代之的是从1.7开始,增加了另一个共享版的构造方法,这个方法也是包私有的:

String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}

第二个参数目前没有用到,始终为true,仅仅是为了和另一个公有构造方法“String(char[])”相区别才增加了这么一个参数。这个构造方法用来创建一个和原字符串一模一样的字符串,而不是像以前一样可以创建原字符串的一个子串。在这种情况下,共享数组不会导致内存泄漏问题,只是其用处大打折扣,因为只有很少的情况需要创建一个和原字符串一模一样的字符串,多数情况只需使用原字符串即可。这就像构造方法“String(String)”一样,应该很少有人会使用它来创建字符串吧。

总结

原来的substring性能好,但在一些情况下却可能导致严重的内存泄漏。新的substring没有内存泄漏的隐患,因此健壮性更好,但却是通过牺牲性能换来的。

两种实现孰优孰劣还真不好说,因为在大多数情况下都不会遇到所谓的严重内存泄漏的情况,因此大部分时候新的substring都不如原来的好。但对一个运行库来说,健壮性可能更重要一些,毕竟它需要适用于任何可能遇到的情况。

新版Java为什么要修改substring的实现的更多相关文章

  1. java实现批量修改指定文件夹下所有后缀名的文件为另外后缀名的代码

    java实现批量修改指定文件夹下所有后缀名的文件为另外后缀名的代码 作者:Vashon package com.ywx.batchrename; import java.io.File; import ...

  2. JAVA中字符串函数subString的用法小结

    本篇文章主要是对JAVA中字符串函数subString的用法进行了详细的介绍,需要的朋友可以过来参考下,希望对大家有所帮助 String str; str=str.substring(int begi ...

  3. Hadoop使用Java进行文件修改删除操作

    Hadoop使用Java进行文件修改删除操作 学习了:http://blog.csdn.net/menghuannvxia/article/details/44651061 学习了:http://bl ...

  4. Java使用BufferedImage修改图片内容

    1.修改图片的架包 <dependency> <groupId>commons-io</groupId> <artifactId>commons-io& ...

  5. 阿里出品的最新版 Java 开发手册,嵩山版,扫地僧

    说起嵩山,我就想起乔峰,想起慕容复,以及他们两位老爹在少林寺大战的场景.当然了,最令我印象深刻的就是那位默默无闻,却一鸣惊人的扫地僧啊.这次,阿里出品的嵩山版 Java 开发手册的封面就有一个扫地僧, ...

  6. 在Linux上安装最新版java的JDK

    之前写过一篇关于MC建服的文章(http://www.cnblogs.com/apollospotatolikett/p/6149042.html),文章中使用的JDK不是最新的版本,当时没有细说如何 ...

  7. Java for LeetCode 030 Substring with Concatenation of All Words【HARD】

    You are given a string, s, and a list of words, words, that are all of the same length. Find all sta ...

  8. Java实现批量修改文件名称

    import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; /** ...

  9. Java反射之修改常量值

    1. 通过反射修改常量的值 package com.blueStarWei.invoke; import java.lang.reflect.Field; public class ModifyFin ...

随机推荐

  1. sysbench - 单组件式测试工具

    1 安装 > ./configure --with-mysql-includes=/usr/local/mysql/include --with-mysql-libs=/usr/local/my ...

  2. 【hdu4035】Maze

    Portal --> hdu4035 Solution 讲道理不是很懂为啥概d那么喜欢走迷宫qwq (推式子推的很爽的一题?) 首先大力dp列式子 用\(f[i]\)表示从\(i\)到离开的期望 ...

  3. android adb devices 后出现offline问题的解决

    Android在使用adb 命令时有时会发现设备的状态是offline,我的神呀,这怎么让我们调试呢.必须解决这个问题. 解决方式如下: 以下方法请确保你的设备处于调试模式下. 方法一:执行 #adb ...

  4. [DeeplearningAI笔记]卷积神经网络2.5-2.7 Network in Network/1*1卷积/Inception网络/GoogleNet

    4.2深度卷积网络 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考文献 Inception网络 --Szegedy C, Liu W, Jia Y, et al. Going deepe ...

  5. Tensorflow BatchNormalization详解:4_使用tf.nn.batch_normalization函数实现Batch Normalization操作

    使用tf.nn.batch_normalization函数实现Batch Normalization操作 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考文献 吴恩达deeplearnin ...

  6. 往android主项目中添加辅助项目

    一个较大的工程往往需要多个项目组成,便于更好的并行开发和管理,但最后还是要合到一起来发布.那如何往主项目里添加其他辅助项目呢? 通常的做法是将辅助项目打包成jar包,像库一样导入到主项目,但是如果我们 ...

  7. Maven将java项目打包生成可运行jar

    Maven将java项目打包生成可运行jar Maven插件配置 <plugins> <plugin> <groupId>org.apache.maven.plug ...

  8. SpringCloud学习(2)——Rest微服务案例

    创建父工程: microservicecloud  创建公共模块api:microservicecloudapi SQL脚本: 此学习路线总共创建3个库, 分别为clouddb01, clouddb0 ...

  9. 算法专题-STL篇

    这篇文章着重记录c++中STL的用法.主要粗略的介绍其用法,以知识点的形式呈现其功能,不会深入源码分析其工作原理. 排序和检索. sort(a,a+n),对a[0]往后的n个元素(包括a[0])进行排 ...

  10. CF540 D 概率 DP

    石头剪刀布三种生物(?) 随机战斗,最后三方各自只有自己方存活下来的概率是多少. 局面的转移明显,注意任选两方决定战斗时有可能出现选了同个种类的,因此注意排除掉同种的组合,也就是条件概率什么的. /* ...