Java字符串之性能优化

简介:

转自:http://it.deepinmind.com/categories.html


基础类型转化成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<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方法也一起改了。但你希望解析的方法不要改动太大(代码现在是好使的,我为什么要费劲去改它呢?):

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类型的。我们用它来改写一下:

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,代码如下:

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)来保证它的缓冲区足够大,而不会重新调整:

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还是关闭的。

目录
相关文章
|
2月前
|
SQL JSON Java
告别字符串拼接:用Java文本块优雅处理多行字符串
告别字符串拼接:用Java文本块优雅处理多行字符串
356 108
|
4月前
|
SQL JSON Java
告别拼接噩梦:Java文本块让多行字符串更优雅
告别拼接噩梦:Java文本块让多行字符串更优雅
535 82
|
4月前
|
自然语言处理 Java Apache
在Java中将String字符串转换为算术表达式并计算
具体的实现逻辑需要填写在 `Tokenizer`和 `ExpressionParser`类中,这里只提供了大概的框架。在实际实现时 `Tokenizer`应该提供分词逻辑,把输入的字符串转换成Token序列。而 `ExpressionParser`应当通过递归下降的方式依次解析
338 14
|
5月前
|
Java 物联网 数据处理
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
Java Solon v3.2.0 是一款性能卓越的后端开发框架,新版本并发性能提升700%,内存占用节省50%。本文将从核心特性(如事件驱动模型与内存优化)、技术方案示例(Web应用搭建与数据库集成)到实际应用案例(电商平台与物联网平台)全面解析其优势与使用方法。通过简单代码示例和真实场景展示,帮助开发者快速掌握并应用于项目中,大幅提升系统性能与资源利用率。
179 6
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
|
4月前
|
存储 监控 算法
企业上网监控场景下布隆过滤器的 Java 算法构建及其性能优化研究
布隆过滤器是一种高效的数据结构,广泛应用于企业上网监控系统中,用于快速判断员工访问的网址是否为违规站点。相比传统哈希表,它具有更低的内存占用和更快的查询速度,支持实时拦截、动态更新和资源压缩,有效提升系统性能并降低成本。
174 0
|
6月前
|
存储 安全 Java
【高薪程序员必看】万字长文拆解Java并发编程!(4-1):悲观锁底层原理与性能优化实战
目录4. JVM字节码文件4.1. 字节码文件-组成4.1.1. 组成-基础信息4.1.1.1. 基础信息-魔数4.1.1.2. 基础信息-主副版本号4.1.2. 组成-常量池4.1.3. 组成-方法4.1.3.1. 方法-工作流程4.1.4. 组成-字段4.1.5. 组成-属性4.2. 字节码文件-查看工具4.2.1. javap4.2.2. jclasslib4.2.3. 阿里Arthas
114 0
|
8月前
|
存储 缓存 安全
Java 字符串详解
本文介绍了 Java 中的三种字符串类型:String、StringBuffer 和 StringBuilder,详细讲解了它们的区别与使用场景。String 是不可变的字符串常量,线程安全但操作效率较低;StringBuffer 是可变的字符串缓冲区,线程安全但性能稍逊;StringBuilder 同样是可变的字符串缓冲区,但非线程安全,性能更高。文章还列举了三者的常用方法,并总结了它们在不同环境下的适用情况及执行速度对比。
201 17
|
8月前
|
存储 缓存 安全
Java字符串缓冲区
字符串缓冲区是用于处理可变字符串的容器,Java中提供了`StringBuffer`和`StringBuilder`两种实现。由于`String`类不可变,当需要频繁修改字符串时,使用缓冲区更高效。`StringBuffer`是一个线程安全的容器,支持动态扩展、任意类型数据转为字符串存储,并提供多种操作方法(如`append`、`insert`、`delete`等)。通过这些方法,可以方便地对字符串进行添加、插入、删除等操作,最终将结果转换为字符串。示例代码展示了如何创建缓冲区对象并调用相关方法完成字符串操作。
232 13
|
12月前
|
SQL Java 索引
java小工具util系列2:字符串工具
java小工具util系列2:字符串工具
293 83
下一篇
oss云网关配置