Java字符串之性能提升

来源:互联网 发布:淘宝如何判定虚假交易 编辑:程序博客网 时间:2024/05/18 00:07


基础类型转化成String



在程序中你可能时常会需要将别的类型转化成String,有时候可能是一些基础类型的值。在拼接字符串的时候,如果你有两个或者多个基础类型的值需要放到前面,你需要显式的将第一个值转化成String(不然的话像System.out.println(1+'a')会输出98,而不是"1a")。当然了,有一组String.valueOf方法可以完成这个(或者是基础类型对应的包装类的方法),不过如果有更好的方法能少敲点代码的话,谁还会愿意这么写呢?

在基础类型前面拼接上一个空串(""+1)是最简单的方法了。这个表达式的结果就是一个String,在这之后你就可以随意的进行字符串拼接操作了——编译器会自动将那些基础类型全转化成String的。

不幸的是,这是最糟糕的实现方法了。要想知道为什么,我们得先介绍下这个字符串拼接在Java里是如何处理的。如果一个字符串(不管是字面常量也好,或者是变量,方法调用的结果也好)后面跟着一个+号,再后面是任何的类型表达式:


string_exp + any_exp


Java编译器会把它变成:


new StringBuilder().append( string_exp ).append( any_exp ).toString()



如果表达式里有多个+号的话,后面相应也会多多几个StringBuilder.append的调用,最后才是toString方法。

StringBuilder(String)这个构造方法会分配一块16个字符的内存缓冲区。因此,如果后面拼接的字符不超过16的话,StringBuilder不需要再重新分配内存,不过如果超过16个字符的话StringBuilder会扩充自己的缓冲区。最后调用toString方法的时候,会拷贝StringBuilder里面的缓冲区,新生成一个String对象返回。

这意味着基础类型转化成String的时候,最糟糕的情况就是你得创建:一个StringBuilder对象,一个char[16]数组,一个String对象,一个能把输入值存进去的char[]数组。使用String.valueOf的话,至少StringBuilder对象省掉了。

有的时候或许你根本就不需要转化基础类型。比如,你正在解析一个字符串,它是用单引号分隔开的。最初你可能是这么写的:


final int nextComma = str.indexOf("'");


或者是这样:


final int nextComma = str.indexOf('\'');


程序开发完了,需求变更了,需要支持任意的分隔符。当然了,你的第一反应是,得将这个分隔符存到一个String对象中,然后使用String.indexOf方法来进行拆分。我们假设有个预先配置好的分隔符就放到m_separator字段里(译注:能用这个变量名的,应该不是Java开发出身的吧。。)。那么,你解析的代码应该会是这样的:


private static List split( final String str )
{
final List res = new ArrayList( 10 );
int pos, prev = 0;
while ( ( pos = str.indexOf( m_separator, prev ) ) != -1 )
{
res.add( str.substring( prev, pos ) );
prev = pos + m_separator.length(); // start from next char after separator
}
res.add( str.substring( prev ) );
return res;
}


不过后面你发现这个分隔符就只有一个字符。在初始化的时候,你把String m_separator改成了char m_separator,然后把setter方法也一起改了。但你希望解析的方法不要改动太大(代码现在是好使的,我为什么要费劲去改它呢?):


private static List split2( final String str )
{
final List res = new ArrayList( 10 );
int pos, prev = 0;
while ( ( pos = str.indexOf("" + m_separatorChar, prev ) ) != -1 )
{
res.add( str.substring( prev, pos ) );
prev = pos + 1; // start from next char after separator
}
res.add( str.substring( prev ) );
return res;
}


正如你所看到的,indexOf方法的调用被改动了,不过它还是新建出了一个字符串然后传递进去。当然,这么做是错的,因为还有一个indexOf方法是接收char类型而不是String类型的。我们用它来改写一下:


private static List split3( final String str )
{
final List res = new ArrayList( 10 );
int pos, prev = 0;
while ( ( pos = str.indexOf(m_separatorChar, prev ) ) != -1 )
{
res.add( str.substring( prev, pos ) );
prev = pos + 1; // start from next char after separator
}
res.add( str.substring( prev ) );
return res;
}


我们来用上面的三种实现来进行测试,将"abc,def,ghi,jkl,mno,pqr,stu,vwx,yz"这个串解析1000万次。下面是Java 6_41和7_15的运行时间。Java7由于它的String.substring方法线性复杂度的所以运行时间反而增加了。关于这个你可以参考下这里的资料。

可以看到的是,简单的一个重构,明显的缩短了分割字符串所需要的时间(split/split2->split3)。






split
split2
split3




Java 6
4.65 sec
10.34 sec
3.8 sec


Java 7
6.72 sec
8.29 sec
4.37 sec





字符串拼接



本文当然也不能完全不提字符串拼接另外两种方法。第一种是String.concat,这个很少会用到。它内部其实是分配了一个char[],长度就是拼接后的字符串的长度,它将字符串的数据拷贝到里面,最后使用了私有的构造方法来生成了一个新的字符串,这个构造方法不会再对char[]进行拷贝,因此这个方法调用只创建了两个对象,一个是String本身,还有一个就是它内部的char[]。不幸的是,除非你只拼接两个字符串,这个方法才会比较高效一些。

还有一种方法就是使用StringBuilder类,以及它的一系列的append方法。如果你有很多要拼接的值的话,这个方法当然是最快的了。它在Java5中被首度引入,用来替代StringBuffer。它们的主要区别就是StringBuffer是线程安全的,而StringBuilder不是。不过你会经常并发的拼接字符串么难道?

在测试中,我们把0到100000之间的数全部进行了拼接,分别使用了String.concat, +操作符,还有StringBuilder,代码如下:


String res = "";
for ( int i = 0; i < ITERS; ++i )
{
final String s = Integer.toString( i );
res = res.concat( s ); //second option: res += s;
}
//third option:
StringBuilder res = new StringBuilder();
for ( int i = 0; i < ITERS; ++i )
{
final String s = Integer.toString( i );
res.append( s );
}





String.concat
+
StringBuilder.append




10.145 sec
42.677 sec
0.012 sec




结果非常明显——O(n)的时间复杂度明显要比O(n2) 要强得多。不过在实际工作中会用到大量的+操作符——因为它们实在是非常方便。为了解决这个问题,从Java6 update 20开始,引入了一个-XX:+OtimizeStringConcat开关。在Java 7_02和Java 7_15之间的版本,它是默认打开着的(在Java 6_41中还是默认关闭着的),因此可能你得手动将它打开。跟其它-XX的选项一样,它的文档也相当的差:

Optimize String concatenation operations where possible. (Introduced in Java 6 Update 20)

我们假设Oracle的工程师实现这个选项的时候是尽了最大努力的吧。坊间传闻,它是把一些StringBuilder拼接的逻辑替换成了类似String.concat那样的实现——它先生成一个合适大小的char[]然后再把东西拷贝进去。最后生成一个String。那些嵌套的拼接操作它可能也支持(str1 +(str2+str3) +str4)。打开这个选项后进行测试,结果表明,+号的性能跟String.concat的十分接近:





String.concat
+
StringBuilder.append




10.19 sec
10.722 sec
0.013 sec




我们做另外一个测试。正如前面提到的,默认的StringBuilder构造器分配的是16个字符的缓冲区。当需要添加第17个字符时,这个缓冲区会被扩充。我们把100到100000间的数字分别追加到"12345678901234”的后面。结果串的长度应该是在17到20之间,因此默认的+操作符的实现会需要StringBuilder重新调整大小。作为对比,我们再做另一个测试,在这里我们直接创建一个StringBuilder(21)来保证它的缓冲区足够大,而不会重新调整:


final String s = BASE + i;
final String s = new StringBuilder( 21 ).append( BASE ).append( i ).toString();



没有打开这个选项的话,+号的实现会比显式的StringBuilder的实现的时间要多出一半。打开了这个选项后,两边的结果是一样的。不过有趣的是,即使是StringBuilder的实现本身,打开了开关后速度居然也变快了!




+, 开关关闭
+, 开关打开
new StringBuilder(21),开关关闭
new StringBuilder(21),开关打开





0.958 sec
0.494 sec
0.663 sec
0.494 sec





总结




  • 当转化成字符串的时候,应当避免使用""串进行转化。使用合适的String.valueOf方法或者包装类的toString(value)方法。
  • 尽量使用StringBuilder进行字符串拼接。检查下老旧码,把那些能替换掉的StringBuffer也替换成它。
  • 使用Java 6 update 20引入的-XX:+OptimizeStringConcat选项来提高字符串拼接的性能。在最近的Java7的版本中已经默认打开了,不过在Java 6_41还是关闭的。




原创文章转载请注明出处:http://it.deepinmind.com

英文原文链接

想及时了解博客更新,可以关注我的微博Java译站

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 新鲜鸡肉有点臭怎么办 手机屏幕自己乱点怎么办 电脑开机出现f1f2怎么办 普通话证书身份证号有误怎么办 湖南身份证在深圳怎么办 超市快过期东西怎么办 超市打碎了东西怎么办 毕业证与身份证号不符怎么办 家里电路坏了怎么办 深圳户口没房怎么办 深圳租凭合同怎么办 房屋续租水电费怎么办 二手房买到凶宅怎么办 公租房退休之后怎么办 深圳公务员无房怎么办 深圳安居房回执遗失怎么办 社保网注册不了怎么办 深圳怎么办少儿医保卡 少儿没缴纳医保怎么办 上海辞职后社保怎么办 辞职去上学社保怎么办 辞职后异地社保怎么办 个人不想交社保怎么办 离职去异地社保怎么办 离职到外省社保怎么办 辞职去外省社保怎么办 永康光伏补贴怎么办? 找到工作后档案怎么办 夫妻离婚后户口怎么办 异地档案辞职后怎么办 去网吧没有招聘怎么办 建筑工地老板不给钱怎么办 2018高压电工证怎么办 深圳户口怎么办户口卡 在深圳找不到工作怎么办 亲戚户口不迁出怎么办 深圳集体户口准生证怎么办 天津集体户口怎么办准生证 深圳集体户离职户口怎么办 解析包出现错误怎么办 信息登记号没有怎么办