Java字符串之性能优化
基础类型转化成String
在程序中你可能时常会需要将别的类型转化成String,有时候可能是一些基础类型的值。在拼接字符串的时候,如果你有两个或者多个基础类型的值需要放到前面,你需要显式的将第一个值转化成String(不然的话像System.out.println(1+’a')会输出98,而不是”1a”)。当然了,有一组String.valueOf方法可以完成这个(或者是基础类型对应的包装类的方法),不过如果有更好的方法能少敲点代码的话,谁还会愿意这么写呢?
在基础类型前面拼接上一个空串(”"+1)是最简单的方法了。这个表达式的结果就是一个String,在这之后你就可以随意的进行字符串拼接操作了——编译器会自动将那些基础类型全转化成String的。
不幸的是,这是最糟糕的实现方法了。要想知道为什么,我们得先介绍下这个字符串拼接在Java里是如何处理的。如果一个字符串(不管是字面常量也好,或者是变量,方法调用的结果也好)后面跟着一个+号,再后面是任何的类型表达式:
|
1
|
string_exp + any_exp |
Java编译器会把它变成:
|
1
|
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对象省掉了。
有的时候或许你根本就不需要转化基础类型。比如,你正在解析一个字符串,它是用单引号分隔开的。最初你可能是这么写的:
|
1
|
final int nextComma = str.indexOf("'"); |
或者是这样:
|
1
|
final int nextComma = str.indexOf('\''); |
程序开发完了,需求变更了,需要支持任意的分隔符。当然了,你的第一反应是,得将这个分隔符存到一个String对象中,然后使用String.indexOf方法来进行拆分。我们假设有个预先配置好的分隔符就放到m_separator字段里(译注:能用这个变量名的,应该不是Java开发出身的吧。。)。那么,你解析的代码应该会是这样的:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
private static List<String> split( final String str ){ final List<String> res = new ArrayList<String>( 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方法也一起改了。但你希望解析的方法不要改动太大(代码现在是好使的,我为什么要费劲去改它呢?):
|
1
2
3
4
5
6
7
8
9
10
11
12
|
private static List<String> split2( final String str ){ final List<String> res = new ArrayList<String>( 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类型的。我们用它来改写一下:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
private static List<String> split3( final String str ){ final List<String> res = new ArrayList<String>( 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 |
字符串拼接
本文当然也不能完全不提字符串拼接另外两种方法。第一种是String.concat,这个很少会用到。它内部其实是分配了一个char[],长度就是拼接后的字符串的长度,它将字符串的数据拷贝到里面,最后使用了私有的构造方法来生成了一个新的字符串,这个构造方法不会再对char[]进行拷贝,因此这个方法调用只创建了两个对象,一个是String本身,还有一个就是它内部的char[]。不幸的是,除非你只拼接两个字符串,这个方法才会比较高效一些。
还有一种方法就是使用StringBuilder类,以及它的一系列的append方法。如果你有很多要拼接的值的话,这个方法当然是最快的了。它在Java5中被首度引入,用来替代StringBuffer。它们的主要区别就是StringBuffer是线程安全的,而StringBuilder不是。不过你会经常并发的拼接字符串么难道?
在测试中,我们把0到100000之间的数全部进行了拼接,分别使用了String.concat, +操作符,还有StringBuilder,代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 702和Java 715之间的版本,它是默认打开着的(在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)来保证它的缓冲区足够大,而不会重新调整:
|
1
2
|
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还是关闭的。
Java字符串之性能优化的更多相关文章
- Java服务端性能优化
<Java程序性能优化>说性能优化包含五个层次:设计调优.代码调优.JVM调优.数据库调优.操作系统调优. 常用的几个代码优化方案: 使用单例 对于IO处理.数据库连接.配置文件解析加载等 ...
- java反射之-性能优化
在最近的计划中,打算看看在不使用google protobuf的情况下,在原有的采用jackson作为json序列化工具的基础上,是否可以实现进一步的性能优化.主要是针对list的情况. 测试的时候选 ...
- 人人都能掌握的Java服务端性能优化方案
作为一个Java后端开发,我们写出的大部分代码都决定着用户的使用体验.如果我们的后端代码性能不好,那么用户在访问我们的网站时就要浪费一些时间等待服务器的响应.这就可能导致用户投诉甚至用户的流失. 关于 ...
- java字符串格式化性能对比String.format/StringBuilder/+拼接
String.format由于每次都有生成一个Formatter对象,因此速度会比较慢,在大数据量需要格式化处理的时候,避免使用String.format进行格式化,相反使用StringUtils.l ...
- Java开发代码性能优化总结
代码优化,可能说起来一些人觉得没用.可是我觉得应该平时开发过程中,就尽量要求自己,养成良好习惯,一个个小的优化点,积攒起来绝对是有大幅度效率提升的.好了,将平时看到用到总结的分享给大家. 代码优化的目 ...
- Java编程代码性能优化总结
如果有足够的时间开发.维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的. 代码优化的目标是: 1.减小代码的体积 2.提高代码运行的效率 ...
- java反射机制性能优化
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.uti ...
- java流的性能优化1-文件复制
传统的I/O速度相对照较慢,它会成为系统性能的瓶颈,所以在java1.4之后提供了NIO,它是一种全新的流:它具有下面特性: 1.为全部的原是类型提供Buffer缓存支持: 2.使用java.nio. ...
- 性能优化之Java(Android)代码优化
最新最准确内容建议直接访问原文:性能优化之Java(Android)代码优化 本文为Android性能优化的第三篇——Java(Android)代码优化.主要介绍Java代码中性能优化方式及网络优化, ...
随机推荐
- zabbix电话告警V1
最近决定将夜班值班人员取消,夜里告警采用机器人电话通知.总结一下这么几个情况,有问题还请指出,希望也能给大家多一个思路. V1做的太糙了,预计年初上V2 一.用谁家的服务 费用我没有太关注,主要就是看 ...
- MSP430常见问题之电源类
Q1:msp430(我用的4619)的VCC,DVCC,VSS,DVSS怎么接啊?模拟的和数字的一样吗?A1:CC 就是正,SS 就是负,A是模拟电,D 是数字电,A的都接在一起,D 的都接在一起,地 ...
- 文件加载---理解一个project的第一步
当我最开始写php的时候,总是担心这个问题:我在这儿new的一个class能加载到对应的类文件吗?毕竟一运行就报Fatal Error,什么**文件没找到,类无法实例化等等是一种很“低级”的错误,怕别 ...
- AIDL实现Android IPC
1.AIDL文本解释 在软件工程中,接口定义语言(IDL)已经成为通用术语,是用来描述软件组件接口的特定语言.在Android中,该IDL被称为Android接口定义语言(AIDL),它是纯文本文件, ...
- web工程中URL地址的写法
在开发中我们不可避免的要碰到许多需要写URL地址的情况,这常常让我们感到头疼.下面笔者推荐一种简单的做法.URL地址分为绝对路径和相对路径两种.相对路径又分为相对资源路径和相对根路径.显然绝对路径在开 ...
- response小结(一)——用response向客户端输出中文数据(乱码问题分析)
Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象,和代表响应的response对象.request和response对象既然代表请求和响应,那我们要 ...
- VHD轻松实现双系统
VHD 是微软虚拟磁盘文件. VHD(Microsoft Virtual Hard Disk format). 目前可以使用Microsoft Virtual PC 2007 and Micros ...
- 文件流操作(FileStream,StreamReader,StreamWriter)
大文件拷贝: /// <summary> /// 大文件拷贝 /// </summary> /// <param name="sSource"> ...
- Oracle中存储过程传入表名学习
Oracle中存储过程传入表名: 一.动态清除该表的数据 create or replace procedure p_deletetable(i_tableName in varchar2) as ...
- IOS 图片全屏预览
如果你感觉累,那就对了那是因为你在走上坡路..这句话似乎有点道理的样子,时常提醒自己无论走到哪都不要忘记自己当初为什么出发.有时想想感觉有的东西可以记录一下,就把它记录下来吧,这次想写一下关于单张图片 ...