给定下面的2个toString()实现,哪个是首选的:

public String toString(){
    return "{a:"+ a + ", b:" + b + ", c: " + c +"}";
}

or

public String toString(){
    StringBuilder sb = new StringBuilder(100);
    return sb.append("{a:").append(a)
          .append(", b:").append(b)
          .append(", c:").append(c)
          .append("}")
          .toString();
}

?

更重要的是,鉴于我们只有3个属性,它可能不会有什么不同,但在什么时候你会从+ concat切换到StringBuilder?


当前回答

对于当前的编译器是否仍然需要使用StringBuilder,似乎存在一些争论。所以我想谈谈我的经验。

我有一个包含10k条记录的JDBC结果集(是的,我需要将它们全部放在一个批处理中)。在我的Java 1.8机器上使用+运算符大约需要5分钟。对于相同的查询,使用stringBuilder.append("")只需不到一秒。

所以差异是巨大的。在循环中,StringBuilder要快得多。

其他回答

这取决于字符串的大小。

请看下面的例子:

static final int MAX_ITERATIONS = 50000;
static final int CALC_AVG_EVERY = 10000;

public static void main(String[] args) {
    printBytecodeVersion();
    printJavaVersion();
    case1();//str.concat
    case2();//+=
    case3();//StringBuilder
}

static void case1() {
    System.out.println("[str1.concat(str2)]");
    List<Long> savedTimes = new ArrayList();
    long startTimeAll = System.currentTimeMillis();
    String str = "";
    for (int i = 0; i < MAX_ITERATIONS; i++) {
        long startTime = System.currentTimeMillis();
        str = str.concat(UUID.randomUUID() + "---");
        saveTime(savedTimes, startTime);
    }
    System.out.println("Created string of length:" + str.length() + " in " + (System.currentTimeMillis() - startTimeAll) + " ms");
}

static void case2() {
    System.out.println("[str1+=str2]");
    List<Long> savedTimes = new ArrayList();
    long startTimeAll = System.currentTimeMillis();
    String str = "";
    for (int i = 0; i < MAX_ITERATIONS; i++) {
        long startTime = System.currentTimeMillis();
        str += UUID.randomUUID() + "---";
        saveTime(savedTimes, startTime);
    }
    System.out.println("Created string of length:" + str.length() + " in " + (System.currentTimeMillis() - startTimeAll) + " ms");
}

static void case3() {
    System.out.println("[str1.append(str2)]");
    List<Long> savedTimes = new ArrayList();
    long startTimeAll = System.currentTimeMillis();
    StringBuilder str = new StringBuilder("");
    for (int i = 0; i < MAX_ITERATIONS; i++) {
        long startTime = System.currentTimeMillis();
        str.append(UUID.randomUUID() + "---");
        saveTime(savedTimes, startTime);
    }
    System.out.println("Created string of length:" + str.length() + " in " + (System.currentTimeMillis() - startTimeAll) + " ms");

}

static void saveTime(List<Long> executionTimes, long startTime) {
    executionTimes.add(System.currentTimeMillis() - startTime);
    if (executionTimes.size() % CALC_AVG_EVERY == 0) {
        out.println("average time for " + executionTimes.size() + " concatenations: "
                + NumberFormat.getInstance().format(executionTimes.stream().mapToLong(Long::longValue).average().orElseGet(() -> 0))
                + " ms avg");
        executionTimes.clear();
    }
}

输出:

java bytecode version:8 java.version: 1.8.0_144 [str1.concat(str2)] average time for 10000 concatenations: 0.096 ms avg average time for 10000 concatenations: 0.185 ms avg average time for 10000 concatenations: 0.327 ms avg average time for 10000 concatenations: 0.501 ms avg average time for 10000 concatenations: 0.656 ms avg Created string of length:1950000 in 17745 ms [str1+=str2] average time for 10000 concatenations: 0.21 ms avg average time for 10000 concatenations: 0.652 ms avg average time for 10000 concatenations: 1.129 ms avg average time for 10000 concatenations: 1.727 ms avg average time for 10000 concatenations: 2.302 ms avg Created string of length:1950000 in 60279 ms [str1.append(str2)] average time for 10000 concatenations: 0.002 ms avg average time for 10000 concatenations: 0.002 ms avg average time for 10000 concatenations: 0.002 ms avg average time for 10000 concatenations: 0.002 ms avg average time for 10000 concatenations: 0.002 ms avg Created string of length:1950000 in 100 ms

随着字符串长度的增加,+=和.concat的连接时间也会增加,后者效率更高,但仍然是非常量 这就是绝对需要StringBuilder的地方。

附注:我不认为什么时候在Java中使用StringBuilder是一个真正的复制。 这个问题讨论的是toString(),它在大多数情况下不会执行大字符串的连接。


2019年更新

自java8时代以来,情况发生了一些变化。现在看来(java13), +=的连接时间实际上与str.concat()相同。但是StringBuilder的连接时间仍然是固定的。(上面的原始帖子略有编辑,添加了更多详细的输出)

java bytecode version:13 java.version: 13.0.1 [str1.concat(str2)] average time for 10000 concatenations: 0.047 ms avg average time for 10000 concatenations: 0.1 ms avg average time for 10000 concatenations: 0.17 ms avg average time for 10000 concatenations: 0.255 ms avg average time for 10000 concatenations: 0.336 ms avg Created string of length:1950000 in 9147 ms [str1+=str2] average time for 10000 concatenations: 0.037 ms avg average time for 10000 concatenations: 0.097 ms avg average time for 10000 concatenations: 0.249 ms avg average time for 10000 concatenations: 0.298 ms avg average time for 10000 concatenations: 0.326 ms avg Created string of length:1950000 in 10191 ms [str1.append(str2)] average time for 10000 concatenations: 0.001 ms avg average time for 10000 concatenations: 0.001 ms avg average time for 10000 concatenations: 0.001 ms avg average time for 10000 concatenations: 0.001 ms avg average time for 10000 concatenations: 0.001 ms avg Created string of length:1950000 in 43 ms

值得注意的还有bytecode:8/java。与bytecode:8/java.version:8相比,Version:13组合具有良好的性能优势

关键在于你是在一个地方写一个连接,还是在一段时间内把它积累起来。

对于您给出的示例,显式使用StringBuilder是没有意义的。(请查看第一个案例的编译代码。)

但是如果你正在构建一个字符串,例如在一个循环中,使用StringBuilder。

为了澄清,假设hugeArray包含数千个字符串,代码如下:

...
String result = "";
for (String s : hugeArray) {
    result = result + s;
}

与以下相比非常浪费时间和内存:

...
StringBuilder sb = new StringBuilder();
for (String s : hugeArray) {
    sb.append(s);
}
String result = sb.toString();

使用最新版本的Java(1.8)的反汇编(javap -c)显示了编译器引入的优化。+以及sb.append()将生成非常相似的代码。然而,如果我们在for循环中使用+,检查行为将是值得的。

在for循环中使用+添加字符串

Java:

public String myCatPlus(String[] vals) {
    String result = "";
    for (String val : vals) {
        result = result + val;
    }
    return result;
}

ByteCode:(用于循环摘录)

12: iload         5
14: iload         4
16: if_icmpge     51
19: aload_3
20: iload         5
22: aaload
23: astore        6
25: new           #3                  // class java/lang/StringBuilder
28: dup
29: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
32: aload_2
33: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: aload         6
38: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: astore_2
45: iinc          5, 1
48: goto          12

使用stringbuilder.append添加字符串

Java:

public String myCatSb(String[] vals) {
    StringBuilder sb = new StringBuilder();
    for(String val : vals) {
        sb.append(val);
    }
    return sb.toString();
}

ByteCdoe:(循环摘录)

17: iload         5
19: iload         4
21: if_icmpge     43
24: aload_3
25: iload         5
27: aaload
28: astore        6
30: aload_2
31: aload         6
33: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: pop
37: iinc          5, 1
40: goto          17
43: aload_2

不过,两者还是有一些明显的区别。在第一种情况下,使用了+,为每次for循环迭代创建新的StringBuilder,并通过执行toString()调用存储生成的结果(29到41)。所以当你在for循环中使用+运算符时,你会生成你真正不需要的中间字符串。

对于像这样的简单字符串,我更喜欢使用

"string".concat("string").concat("string");

按照顺序,我认为构造字符串的首选方法是使用StringBuilder, string# concat(),然后是重载的+运算符。在处理大字符串时,StringBuilder显著提高了性能,就像使用+操作符会大幅降低性能一样(随着字符串大小的增加,性能会呈指数级下降)。使用.concat()的一个问题是它会抛出nullpointerexception。

Apache Commons-Lang有一个超级容易使用的ToStringBuilder类。它在处理附加逻辑以及格式化你想要的toString外观方面做得很好。

public void toString() {
     ToStringBuilder tsb =  new ToStringBuilder(this);
     tsb.append("a", a);
     tsb.append("b", b)
     return tsb.toString();
}

将返回类似com.blah的输出。YourClass@abc1321f (a =, = foo)。

或者使用链接的更浓缩的形式:

public void toString() {
     return new ToStringBuilder(this).append("a", a).append("b", b").toString();
}

或者如果你想使用反射来包含类的每个字段:

public String toString() {
    return ToStringBuilder.reflectionToString(this);
}

如果需要,还可以自定义ToString的样式。