为什么StringBuilder比String更快?

15

为什么使用 StringBuilder 比使用 + 运算符连接字符串要快得多?即使内部实现 + 运算符时使用了 StringBufferStringBuilder

public void shortConcatenation(){
    long startTime = System.currentTimeMillis();
    while (System.currentTimeMillis() - startTime <= 1000){

        character += "Y";
    }

    System.out.println("short: " + character.length());
}

使用字符串构建器

 public void shortConcatenation2(){
    long startTime = System.currentTimeMillis();
    StringBuilder sb = new StringBuilder();
    while (System.currentTimeMillis() - startTime <= 1000){

        sb.append("Y");
    }
    System.out.println("string builder short: " + sb.length());
}

我知道这里有很多类似的问题,但它们并不能真正回答我的问题。


5
如果那些回答不能解答你的问题,这里也不会有任何其他回答。 - Sotirios Delimanolis
3
您有哪些证据来支持这个说法:StringBuilder 比使用 + 运算符进行字符串连接的速度要快得多? - NPE
1
我已经编写了一段代码,使用了"+"运算符和StringBuilder。请查看并尝试运行它以查看结果。 - Jason
2
请注意,您的时间测量方式存在严重缺陷。在像Java这样的语言中,由于JVM、JIT和GC的存在,您不能做出这样的事情。您的测量结果可能有90%只是副作用。我已经看到很多次有人使用这种方法进行测量,得出“10ms”与“100ms”的结果,并得出结论第一个肯定更快,尽管实际上它实际上要慢得多。您必须使用像JMH这样的框架才能获得可用的结果。 - Zabuzard
https://dev59.com/nXA85IYBdhLWcg3wCe9Z - Sathvik
4个回答

27

你理解它的内部工作原理了吗?

每次执行 stringA += stringB; 都会创建一个新的字符串并将其赋给 stringA,因此它将消耗内存(一个新的字符串实例!)和时间(复制旧字符串+另一个字符串的新字符)。

StringBuilder 在内部使用字符数组,当使用 .append() 方法时,它将执行以下几个操作:

  • 检查字符串附加是否有任何可用空间
  • 再进行一些内部检查,并运行 System.arraycopy 来将字符串的字符复制到数组中。

就我个人而言,每次分配新字符串(创建新字符串实例、放置字符串等)可能在内存和速度上都非常昂贵(尤其是在 while/for 等循环中)。

在你的示例中,使用 StringBuilder 更好,但如果你需要(例如)像 .toString() 这样的简单东西,则可以使用。

public String toString() {
    return StringA + " - " + StringB;
}

并没有什么区别(在这种情况下最好避免使用StringBuilder开销,因为它在这里是无用的)。


20

Java中的字符串是不可变的immutable,这意味着操作字符串的方法永远无法改变字符串的值。通过使用+=进行的字符串连接是通过为完全新的字符串分配内存来完成的,该字符串是两个先前字符串的连接,并用此新字符串替换引用。每个新连接都需要构造一个全新的String对象。

相比之下,StringBuilder和StringBuffer类作为一系列mutable字符实现。这意味着当您将新字符串或字符附加到StringBuilder时,它仅更新其内部数组以反映您所做的更改。这意味着只有在字符串超过StringBuilder中已存在的缓冲区时才会分配新内存。


1
我可以列举一个很好的例子来理解这个问题(我的意思是我认为这是一个很好的例子)。请查看以下代码,它来自于LeetCode的一个问题:https://leetcode.com/problems/remove-outermost-parentheses/

1:使用字符串

public String removeOuterParentheses(String S) {

    String a = "";
    int num = 0;
    for(int i=0; i < S.length()-1; i++) {
        if(S.charAt(i) == '(' && num++ > 0) {
            a += "(";
        }
        if(S.charAt(i) == ')' && num-- > 1) {
            a += ")";
        }
    }
    return a;
}

现在,使用 StringBuilder。
public String removeOuterParentheses(String S) {

    StringBuilder sb = new StringBuilder();

    int a = 0;
    for(char ch : S.toCharArray()) {
        if(ch == '(' && a++ > 0) sb.append('(');
        if(ch == ')' && a-- > 1) sb.append(')');
    }
    return sb.toString();
}

两者的表现差距很大。第一个提交使用 String,而后一个使用 StringBuilder。

Enter image description here

如上所述,理论是一样的。字符串按属性是不可变和同步的,即其状态不能被改变。例如,第二个是昂贵的,因为每当使用连接函数或“+”时都会创建一个新的内存分配。它将消耗大量堆,并且换取较慢的速度。相比之下,StringBuilder是可变的,它只会添加而不会对消耗的内存创建负担。

0
问题是你使用了比编译器更高效的 StringBuilder 版本。试试看。
while (System.currentTimeMillis() - startTime <= 1000) {
    character = new StringBuilder(character).append("Y").toString();
}

这将给你一个类似于+=的结果。问题是编译器并不一定聪明到意识到,它可以用同一个StringBuilder来完成所有的操作,而不是反复地进行StringBuilderString之间的转换。正如其他人已经指出的那样,使用一个单独的StringBuilder会更快,因为它只需要偶尔分配内存。 +=版本可能会进行更多的优化。但它的行为更接近于上述版本。请考虑。
sb = new StringBuilder();
while (System.currentTimeMillis() - startTime <= 1000) {
    sb.append("Y");
    character = sb.toString();
}

几乎和你的版本一样,但是每次迭代都像创建一个新的StringBuilder一样慢。显然是toString导致速度变慢。

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