java - string相加 - stringbuilder线程不安全




Java中的toString()中的StringBuilder vs String连接 (12)

鉴于下面的2个toString()实现,哪一个是首选的:

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

要么

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


Apache Commons-Lang有一个ToStringBuilder类,它非常易于使用。 它在处理append逻辑和格式化你的toString的外观方面做得很好。

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

将返回类似[email protected][a=whatever, b=foo]

或者以更简洁的形式使用链接:

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

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

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

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


从Java 1.5开始,使用“+”和StringBuilder.append()生成简单的一行连接就会生成完全相同的字节码。

所以为了代码可读性,使用“+”。

2例外:

  • 多线程环境:StringBuffer
  • 循环中的连接:StringBuilder / StringBuffer

关键在于你是在一个地方编写一个单独的连接还是随着时间的推移累积它。

对于你给出的例子,明确使用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();

出于性能原因,不鼓励使用+=String concatenation)。 原因是:Java String是不可变的,每次新的串联完成时,都会创建一个新的String (新String 与字符串池中已有的旧字符串具有不同的指纹)。 创建新的字符串会给GC带来压力并使程序变慢:对象创建非常昂贵。

下面的代码应该使其更加实用和清晰。

public static void main(String[] args) 
{
    // warming up
    for(int i = 0; i < 100; i++)
        RandomStringUtils.randomAlphanumeric(1024);
    final StringBuilder appender = new StringBuilder();
    for(int i = 0; i < 100; i++)
        appender.append(RandomStringUtils.randomAlphanumeric(i));

    // testing
    for(int i = 1; i <= 10000; i*=10)
        test(i);
}

public static void test(final int howMany) 
{
    List<String> samples = new ArrayList<>(howMany);
    for(int i = 0; i < howMany; i++)
        samples.add(RandomStringUtils.randomAlphabetic(128));

    final StringBuilder builder = new StringBuilder();
    long start = System.nanoTime();
    for(String sample: samples)
        builder.append(sample);
    builder.toString();
    long elapsed = System.nanoTime() - start;
    System.out.printf("builder - %d - elapsed: %dus\n", howMany, elapsed / 1000);

    String accumulator = "";
    start = System.nanoTime();
    for(String sample: samples)
        accumulator += sample;
    elapsed = System.nanoTime() - start;
    System.out.printf("concatenation - %d - elapsed: %dus\n", howMany, elapsed / (int) 1e3);

    start = System.nanoTime();
    String newOne = null;
    for(String sample: samples)
        newOne = new String(sample);
    elapsed = System.nanoTime() - start;
    System.out.printf("creation - %d - elapsed: %dus\n\n", howMany, elapsed / 1000);
}

运行结果报告如下。

builder - 1 - elapsed: 132us
concatenation - 1 - elapsed: 4us
creation - 1 - elapsed: 5us

builder - 10 - elapsed: 9us
concatenation - 10 - elapsed: 26us
creation - 10 - elapsed: 5us

builder - 100 - elapsed: 77us
concatenation - 100 - elapsed: 1669us
creation - 100 - elapsed: 43us

builder - 1000 - elapsed: 511us
concatenation - 1000 - elapsed: 111504us
creation - 1000 - elapsed: 282us

builder - 10000 - elapsed: 3364us 
concatenation - 10000 - elapsed: 5709793us
creation - 10000 - elapsed: 972us

不考虑1个级联的结果(JIT还没有完成其工作),即使是10个级联,性能损失也是相关的; 成千上万的连接,差异是巨大的。

从这个非常快速的实验中学到的经验教训(很容易用上面的代码重现):即使在需要几个连接的非常基本的情况下,也不要使用+=连接字符串(正如所说的,创建新字符串无论如何都是昂贵的,在GC上)。


在大多数情况下,您不会看到两种方法之间存在实际差异,但很容易构建这种最糟糕的情况:

public class Main
{
    public static void main(String[] args)
    {
        long now = System.currentTimeMillis();
        slow();
        System.out.println("slow elapsed " + (System.currentTimeMillis() - now) + " ms");

        now = System.currentTimeMillis();
        fast();
        System.out.println("fast elapsed " + (System.currentTimeMillis() - now) + " ms");
    }

    private static void fast()
    {
        StringBuilder s = new StringBuilder();
        for(int i=0;i<100000;i++)
            s.append("*");      
    }

    private static void slow()
    {
        String s = "";
        for(int i=0;i<100000;i++)
            s+="*";
    }
}

输出是:

slow elapsed 11741 ms
fast elapsed 7 ms

问题在于+ =附加到一个字符串会重新构建一个新的字符串,所以它会使字符串的长度(两者的总和)成线性关系。

所以 - 对你的问题:

第二种方法会更快,但它的可读性较差,难以维护。 正如我所说,在你的具体情况下,你可能不会看到区别。


对于我喜欢使用的简单字符串

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

为了说明,构建字符串的首选方法是使用StringBuilder,String#concat(),然后重载+运算符。 StringBuilder在处理大型字符串时性能显着提高,就像使用+运算符一样,性能大幅下降(随着字符串大小的增加呈指数级大幅下降)。 使用.concat()的一个问题是它可以抛出NullPointerExceptions。


性能明智使用'+'的字符串连接的代价很高,因为它必须创建String的全新副本,因为字符串在Java中是不可变的。 如果连接非常频繁,这会起到特殊的作用,例如:在循环内部。 以下是当我尝试做这样的事情时,我的IDEA建议的内容:


我可以指出,如果您要迭代集合并使用StringBuilder,您可能需要查看Apache Commons LangStringUtils.join() (以不同的风格)?

无论性能如何,它都可以节省您创建StringBuilders和循环,看起来像是第一百万次。


我比较了四种不同的方法来比较性能。 我完全不知道gc会发生什么,但对我来说重要的是时间。 编译器是重要的因素。我在window8.1平台下使用了jdk1.8.0_45。

concatWithPlusOperator = 8
concatWithBuilder = 130
concatWithConcat = 127
concatStringFormat = 3737
concatWithBuilder2 = 46


public class StringConcatenationBenchmark {

private static final int MAX_LOOP_COUNT = 1000000;

public static void main(String[] args) {

    int loopCount = 0;
    long t1 = System.currentTimeMillis();
    while (loopCount < MAX_LOOP_COUNT) {
        concatWithPlusOperator();
        loopCount++;
    }
    long t2 = System.currentTimeMillis();
    System.out.println("concatWithPlusOperator = " + (t2 - t1));

    long t3 = System.currentTimeMillis();
    loopCount = 0;
    while (loopCount < MAX_LOOP_COUNT) {
        concatWithBuilder();
        loopCount++;
    }
    long t4 = System.currentTimeMillis();
    System.out.println("concatWithBuilder = " + (t4 - t3));

    long t5 = System.currentTimeMillis();
    loopCount = 0;
    while (loopCount < MAX_LOOP_COUNT) {
        concatWithConcat();
        loopCount++;
    }
    long t6 = System.currentTimeMillis();
    System.out.println("concatWithConcat = " + (t6 - t5));

    long t7 = System.currentTimeMillis();
    loopCount = 0;
    while (loopCount < MAX_LOOP_COUNT) {
        concatStringFormat();
        loopCount++;
    }
    long t8 = System.currentTimeMillis();
    System.out.println("concatStringFormat = " + (t8 - t7));

    long t9 = System.currentTimeMillis();
    loopCount = 0;
    while (loopCount < MAX_LOOP_COUNT) {
        concatWithBuilder2();
        loopCount++;
    }
    long t10 = System.currentTimeMillis();
    System.out.println("concatWithBuilder2 = " + (t10 - t9));
}

private static void concatStringFormat() {
    String s = String.format("%s %s %s %s ", "String", "String", "String", "String");
}

private static void concatWithConcat() {
    String s = "String".concat("String").concat("String").concat("String");
}

private static void concatWithBuilder() {
    StringBuilder builder=new StringBuilder("String");
    builder.append("String").append("String").append("String");
    String s = builder.toString();
}

private static void concatWithBuilder2() {
    String s = new StringBuilder("String").append("String").append("String").append("String").toString();
}

private static void concatWithPlusOperator() {
    String s = "String" + "String" + "String" + "String";
}
}

我认为我们应该使用StringBuilder追加方法。 原因是

  1. 字符串连接会每次创建一个新的字符串对象(因为字符串是不可变的对象),所以它会创建3个对象。

  2. 在String构建器中,只会创建一个对象[StringBuilder可变],并且后续字符串会被附加到它。


版本1是更可取的,因为它更短, 编译器实际上将其转换为版本2 - 没有任何性能差异。

更重要的是,由于我们只有3个属性,它可能没有什么区别,但是在什么时候你从concat切换到builder?

在循环连接的时候 - 通常是编译器StringBuilder替换StringBuilder


目前的编译器是否仍然需要使用StringBuilder似乎存在一些争议。 所以我想我会给我2美分的经验。

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

所以差异是巨大的。 在一个循环内部StringBuilder速度要快得多。







stringbuilder