新版Java为什么要修改substring的实现
作者:网络转载 发布时间:[ 2015/1/28 14:05:59 ] 推荐标签:Java 软件开发
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对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。

sales@spasvo.com