java字符串缩短⽅法_Java字符串之性能优化
基础类型转化成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 mseparator改成了char mseparator,然后把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 641和715的运⾏时间。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
字符串拼接
本⽂当然也不能完全不提字符串拼接另外两种⽅法。第⼀种是at,这个很少会⽤到。它内部其实是分配了⼀个char[],长度就是拼接后的字符串的长度,它将字符串的数据拷贝到⾥⾯,最后使⽤了私有的构造⽅法来⽣成了⼀个新的字符串,这个构造⽅法不会再对char[]进⾏拷贝,因此这个⽅法调⽤只创建了两个对象,⼀个是String本⾝,还有⼀个就是它内部的char[]。不幸的是,除⾮你只拼接两个字符串,这个⽅法才会⽐较⾼效⼀些。
还有⼀种⽅法就是使⽤StringBuilder类,以及它的⼀系列的append⽅法。如果你有很多要拼接的值的话,这个⽅法当然是最快的了。它在Java5中被⾸度引⼊,⽤来替代StringBuffer。它们的主要区别就是StringBuffer是线程安全的,⽽StringBuilder不是。不过你会经常并发的拼接字符串么难道?
在测试中,我们把0到100000之间的数全部进⾏了拼接,分别使⽤了at, +操作符,还有StringBuilder,代码如下:
String res = "";
for ( int i = 0; i < ITERS; ++i )
{
final String s = String( i );
res = at( s ); //second option: res += s;
}
//third option:
StringBuilder res = new StringBuilder();
for ( int i = 0; i < ITERS; ++i )
{
final String s = String( i );
res.append( s );
}
+
StringBuilder.append
10.145 sec
42.677 sec
0.012 sec
结果⾮常明显——O(n)的时间复杂度明显要⽐O(n2) 要强得多。不过在实际⼯作中会⽤到⼤量的+操作符——因为它们实在是⾮常⽅便。为了解决这个问题,从Java6 update 20开始,引⼊了⼀个-XX:+OtimizeStringConcat开关。在Java 702和Java 715之间的版本,它是默认打开着的(在Java 6_41中还是默认关闭着的),因此可能你得⼿动将它打开。跟其它-XX的选项⼀样,它的⽂档也相当的差:java的tostring方法
Optimize String concatenation operations where possible. (Introduced in Java 6 Update 20)
我们假设Oracle的⼯程师实现这个选项的时候是尽了最⼤努⼒的吧。坊间传闻,它是把⼀些StringBuilder拼接的逻辑替换成了类似at那样的实现——它先⽣成⼀个合适⼤⼩的char[]然后再把东西拷贝进去。最后⽣成⼀个String。那些嵌套的拼接操作它可能也⽀持(str1 +(str2+str3) +str4)。打开这个选项后进⾏测试,结果表明,+号的性能跟at的⼗分接近:
+
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还是关闭的。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论