分享

Java 字符串拼接效率分析及最佳实践

 孤独一兵 2016-10-10
  1. java连接字符串有多种方式,比如+操作符, StringBuilder.append 方法,这些方法各有什么优劣(可以适当说明各种方式的实现细节)?

  2. 按照高效的原则,那么java中字符串连接的最佳实践是什么?

  3. 有关字符串处理,都有哪些其他的最佳实践?

废话不多说,直接开始, 环境如下:

JDK版本: 1.8.0_65

CPU: i7 4790`

内存: 16G

直接使用 + 拼接

看下面的代码:

Java 字符串拼接效率分析及最佳实践在上面的代码中,我们使用加号来连接四个字符串,这种字符串拼接的方式优点很明显: 代码简单直观,但是对比 StringBuilderStringBuffer大部分情况下 比后者都低,这里说是 大部分情况下 ,我们用javap工具对上面代码生成的字节码进行反编译看看在编译器对这段代码做了什么。

Java 字符串拼接效率分析及最佳实践

从反编译的结果来看,实际上对字符串使用 + 操作符进行拼接,编译器会在编译阶段把代码优化成使用 StringBuilder 类,并调用 append 方法进行字符串拼接,最后调用 toString 方法,这样看来是否可以认为在一般情况下 其实直接使用+,反正编译器也会帮我优化为使用StringBuilder

StringBuilder 源码分析

答案自然是 不可以 的,原因就在于 StringBuilder 这个类它内部做了些什么时。

我们看一看 StringBuilder 类的构造器

Java 字符串拼接效率分析及最佳实践StringBuilder 提供了4个默认的构造器, 除了无参构造函数外,还提供了另外3个重载版本,而内部都调用父类的 super(int capacity) 构造方法,它的父类是 AbstractStringBuilder ,构造方法如下:

Java 字符串拼接效率分析及最佳实践

可以看到实际上StringBuilder内部使用的是 char数组 来存储数据(String、StringBuffer也是),这里 capacity 的值指定了数组的大小。结合 StringBuilder 的无参构造函数,可以知道默认的大小是 16 个字符。

也就是说如果待拼接的字符串总长度不小于16的字符的话,那么其实直接拼接和我们手动写StringBuilder区别不大,但是我们自己构造StringBuilder类可以指定数组的大小,避免分配过多的内存。

现在我们再看看 StringBuilder.append 方法内部做了什么事:

Java 字符串拼接效率分析及最佳实践直接调用的父类的 append方法

Java 字符串拼接效率分析及最佳实践在这个方法内部调用了 ensureCapacityInternal 方法,当拼接后的字符串总大小大于内部数组 value 的大小时,就必须先扩容才能拼接,扩容的代码如下:

Java 字符串拼接效率分析及最佳实践StringBuilder 在扩容时把容量增大到 当前容量的两倍+2 ,这是很可怕的,如果在构造的时候没有指定容量,那么很有可能在扩容之后占用了浪费大量的内存空间。其次扩容后还调用了 Arrays.copyOf 方法,这个方法把扩容前的数据复制到扩容后的空间内,这样做的原因是: StringBuilder 内部使用 char数组 存放数据,java的数组是不可扩容的,所以只能重新申请一片内存空间,并把已有的数据复制到新的空间去,这里它最终调用了 System.arraycopy 方法来复制,这是一个native方法,底层直接操作内存,所以比我们用循环来复制要块的多,即便如此,大量申请内存空间和复制数据带来的影响也不可忽视。

使用 + 拼接和使用 StringBuilder 比较

Java 字符串拼接效率分析及最佳实践上面这段代码经过优化后相当于:

Java 字符串拼接效率分析及最佳实践一眼就能看出 创建了太多的StringBuilder对象 ,而且在每次循环过后str越来越大,导致每次申请的内存空间越来越大,并且当str长度大于16时,每次都要扩容两次!而实际上 toString 方法在创建 String 对象时,调用了 Arrays.copyOfRange方法来复制数据,此时相当于每执行一次,扩容了两次,复制了3次数据,这样的代价是相当高的。

Java 字符串拼接效率分析及最佳实践

这段代码的执行时间在我的机器上都是0ms(小于1ms)和1ms,而上面那段代码则大约在380ms!效率的差距相当明显。

同样是上面的代码,将循环次数调整为 1000000 时,在我的机器上,有指定 capacity 时耗时大约20ms,没有指定 capacity 时耗时大约29ms,这个差距虽然和直接使用 + 操作符有了很大的提升(且循环次数增大了100倍),但是它依旧会触发多次扩容和复制。

将上面的代码改成使用 StringBuffer ,在我的机器上,耗时大约为33ms,这是因为 StringBuffer 在大部分方法上都加上了 synchronized 关键字来保证线程安全,执行效率有一定程度上的降低。

使用 String.concat 拼接

现在再看这段代码:

Java 字符串拼接效率分析及最佳实践这段代码使用了 String.concat 方法,在我的机器上,执行时间大约为130ms,虽然直接相加要好的多,但是比起使用 StringBuilder 还要太多了,似乎没什么用。其实并不是,在很多时候,我们只需要连接两个字符串,而不是多个字符串的拼接,这个时候使用 String.concat 方法比 StringBuilder 要简洁且效率要高。

Java 字符串拼接效率分析及最佳实践

上面这段是 String.concat 的源码,在这个方法中,调用了一次Arrays.copyOf,并且指定了 len + otherLen ,相当于分配了一次内存空间,并分别从str1和str2各复制一次数据。而如果使用 StringBuilder 并指定 capacity ,相当于分配一次内存空间,并分别从str1和str2各复制一次数据,最后因为调用了 toString 方法,又复制了一次数据。

结论

现在根据上面的分析和测试可以知道:

  1. Java中字符串拼接不要直接使用 + 拼接。

  2. 使用StringBuilder或者StringBuffer时,尽可能准确地估算capacity,并在构造时指定,避免内存浪费和频繁的扩容及复制。

  3. 在没有线程安全问题时使用 StringBuilder , 否则使用 StringBuffer

  4. 两个字符串拼接直接调用 String.concat 性能最好。

关于 String 的其他最佳实践:

  1. equals 时总是把能确定不为空的变量写在左边,如使用 ''.equals(str) 判断空串,避免空指针异常。

  2. 第二点是用来排挤第一点的.. 使用 str != null && str.length() != 0 来判断空串,效率比第一点高。

  3. 在需要把其他对象转换为字符串对象时,使用 String.valueOf(obj) 而不是直接调用 obj.toString() 方法,因为前者已经对空值进行检测了,不会抛出空指针异常。

  4. 使用 String.format() 方法对字符串进行格式化输出。

  5. 在JDK 7及以上版本,可以在 switch 结构中使用字符串了,所以对于较多的比较,使用 switch 代替 if-else

  6. 我暂时想的起来的就这么几个了.. 请大家帮忙补充补充...

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多