StringBuffer的new()方法和delete(0, sb.length())哪个更高效?

14

经常有人认为避免在循环中创建对象是一种良好的实践。

那么,关于 StringBuffer,什么方式最有效率?

StringBuffer sb = new StringBuffer();
ObjectInputStream ois = ...;

for (int i=0;i<1000;i++) {

    for (j=0;i<10;j++) {
        sb.append(ois.readUTF());
    }
    ...

    // Which option is the most efficient? 
    sb = new StringBuffer(); // new StringBuffer instance?
    sb.delete(0,sb.length()); // or deleting content?

}

我的意思是,有人可能会争辩说创建一个对象比遍历一个数组更快。


1
你在性能分析器中测试过它吗?结果如何? - Karel Petranek
请参见http://codereview.stackexchange.com/questions/7575/reusing-stringbuilder-or-creating-a-new-one。 - Flow
3个回答

16

首先,StringBuffer是线程安全的,但相比之下性能不如StringBuilder。StringBuilder不是线程安全的,但因此更快。最后,我更喜欢使用setLength将长度设置为0。

sb.setLength(0)

这与.delete(...)类似,但您实际上不太关心长度。此外,由于它不需要“删除”任何内容,因此可能会更快。创建新的StringBuilder(或StringBuffer)会更低效。每当您看到new时,Java都会创建一个新对象并将其放置在堆上。

注意:在查看了.delete.setLength的实现后,.delete将长度设置为0,而.setLength将所有内容都设置为'\0',因此使用.delete可能会有所减少。


如果StringBuilder分配了一个非常大的内部缓冲区,但由于早期循环迭代产生了一个非常大的字符串,而后续迭代则没有产生这么大的字符串,那么您可能会比必要的时间更长地进行 thrashing,但是保留缓冲区通常是一种净胜利,因为如果后续迭代产生的字符串与早期迭代几乎相同大小,则可以避免一些缓冲区复制。 - Mike Samuel
1
当删除操作清除整个内容时,“delete”不是O(N)。 - Mike Samuel
这是真的,我正在更新我的答案。但只有当len == count时才是真的,如果你要删除所有内容,那么它就是真的。 - Amir Raminfar
1
setLength 不会将所有内容都设置为 '\0'。只有在新的大小大于当前大小且仅针对扩展的内部缓冲区部分时才会这样做。 - benbenw
令人惊讶的是,根据JMH的测试结果,StringBuilder.delete()StringBuilder.setLength(0)快26%。这表明,仅仅通过查看代码就无法确定哪个更快。 - Gili

3

仅补充之前的评论:

从源代码来看,delete() 总是调用 System.arraycopy(),但如果参数为 (0, count),它将调用长度为零的 arraycopy(),这可能没有任何效果。我认为这应该被优化掉,因为我打赌这是最常见的情况,但无论如何。

另一方面,对于 setLength(),调用将通过调用 ensureCapacityInternal()(另一个非常常见的情况,我认为应该被优化掉)增加 StringBuilder 的容量,然后像 delete() 一样截断长度。

最终,两种方法都只是将 count 设置为零。

在这种情况下,两个调用都不会进行任何迭代。两者都进行了不必要的函数调用。但是,ensureCapacityInternal() 是一个非常简单的私有方法,这几乎让编译器将其优化到不存在,因此 setLength() 可能略微更高效。

我非常怀疑创建一个新的 StringBuilder 实例能够像简单地将 count 设置为零那样高效,但我想编译器可能会识别所涉及的模式并将重复实例化转换为重复调用 setLength(0)。但在最好的情况下,这将是一种平衡。而且你要依靠编译器识别这种情况。

执行摘要: setLength(0) 是最有效的。为了达到最大效率,在创建 StringBuilder 时预先分配缓冲区空间。


1

删除方法是这样实现的:

public AbstractStringBuilder delete(int start, int end) {
    if (start < 0)
        throw new StringIndexOutOfBoundsException(start);
    if (end > count)
        end = count;
    if (start > end)
        throw new StringIndexOutOfBoundsException();
    int len = end - start;
    if (len > 0) {
        System.arraycopy(value, start+len, value, start, count-end);
        count -= len;
    }
    return this;
}

正如您所看到的,它没有遍历数组。


2
你认为 System.arraycopy 是做什么的? - Amir Raminfar
@Mike,我不确定你说的 len 会变成0是什么意思。它是根据 end - start 计算得出的,即 sb.length() - 0。你所说的 count-end 变成0是正确的。但通常情况下,你不会为了一个参数集合而计算O(n)。 - Amir Raminfar
@Amir,抱歉我认为没有做任何重要的工作,但我错了原因。当start为0且endcountthis.length()时,你最终会执行System.arraycopy(value, count, value, 0, 0)。最后一个0表示从索引计数到索引0复制0字节。 - Mike Samuel
我已经查看了源代码。在这种特定情况下,arraycopy()什么也不会做,只会返回。你所做的就是浪费了函数调用的开销。 - Edward Falk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接