首页 文章

StringBuilder vs Java中toString()的字符串连接

提问于
浏览
812

鉴于下面的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

18 回答

  • 67

    关键是你是在一个地方写一个连接还是随着时间累积它 .

    对于您给出的示例,明确使用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();
    
  • 8

    性能明智使用''进行字符串连接是比较昂贵的,因为它必须创建一个全新的String副本,因为字符串在java中是不可变的 . 如果连接非常频繁,例如:在循环内部,这会起特殊作用 . 以下是我的IDEA在尝试做这样的事情时的建议:

    enter image description here

    通用规则:

    • 在单个字符串赋值中,使用字符串连接很好 .

    • 如果要循环构建大块字符数据,请转到StringBuffer .

    • 在String上使用=总是比使用StringBuffer效率低,所以它应该响铃警告 - 但在某些情况下,与可读性问题相比,获得的优化可以忽略不计,因此请使用常识 .

    这是围绕这个主题的nice Jon Skeet blog .

  • 1

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

    Adding strings using + in a for loop

    Java的:

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

    ByteCode :( for 循环摘录)

    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
    

    Adding strings using stringbuilder.append

    Java的:

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

    ByteCdoe :( for 循环摘录)

    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
    

    虽然有一点 glaring difference . 在第一种情况下,在使用 + 的情况下,为每个for循环迭代创建新的 StringBuilder ,并通过执行 toString() 调用(29到41)来存储生成的结果 . 因此,在 for 循环中使用 + 运算符时,您正在生成您真正不需要的中间字符串 .

  • -3

    请参阅以下示例:

    //java8
    static void main(String[] args) {
        case1();//str.concat
        case2();//+=
        case3();//StringBuilder
    }
    
    static void case1() {
        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() {
        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() {
        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();
            }
    }
    

    输出:

    10000个连接的平均时间:平均时间为0.096毫秒10000个连接的平均时间:平均时间为0.185毫秒10000个连接的平均时间:平均时间为0.327毫秒10000个连接的平均时间:平均时间为0.501毫秒10000个连接的平均时间:0.656毫秒avg创建的长度字符串:1950000在17745毫秒平均时间为10000个连接:平均时间为0.21毫秒10000个连接:平均时间为0.652毫秒10000个连接平均时间:平均时间为1.129毫秒10000个连接平均时间:平均时间为1.727毫秒10000个连接平均时间:2.302毫秒平均值创建的字符串长度:5600000在60279毫秒10000个连接的平均时间:0.002毫秒平均10000个连接的平均时间:0.002毫秒avg 10000个连接的平均时间:0.002毫秒avg 10000个连接的平均时间:0.002毫秒平均10000个连接的平均时间:0.002 ms avg创建的字符串长度:1950000,100 ms

    As the string length increases, so does the concatenation time.
    这就是 StringBuilder 绝对需要的地方 .
    如你所见,串联: UUID.randomUUID()+"---" ,并不会真正影响时间 .

    P.S.: 我认为When to use StringBuilder in Java确实与此重复 .
    这个问题谈到 toString() ,其中大多数时候都没有执行大字符串的连接 .

  • 2

    我更喜欢:

    String.format( "{a: %s, b: %s, c: %s}", a, b, c );
    

    ...因为它简短易读 .

    我会 not 优化这个速度,除非你在具有非常高的重复次数的循环中使用它 and 测量了性能差异 .

    我同意,如果你必须输出很多参数,这个表格会让人感到困惑(就像其中一条评论所说) . 在这种情况下,我会切换到一个更易读的形式(也许使用ToStringBuilder的apache-commons - 取自matt b的答案)并再次忽略性能 .

  • 24

    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=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的样式 .

  • 7

    对于当前的编译器是否仍然需要使用StringBuilder似乎存在争议 . 所以我想我会给我2美分的经验 .

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

    所以差异很大 . 循环内部 StringBuilder 要快得多 .

  • 20

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

    无论性能如何,它都将为您节省必须创建StringBuilders和for循环,这似乎是第百万次 .

  • 7

    我比较了四种不同的方法来比较性能 . 我完全不知道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";
    }
    }
    
  • -3

    我认为我们应该采用StringBuilder附加方法 . 原因是

    • String连接每次都会创建一个新的字符串对象(As String是不可变对象),因此它将创建3个对象 .

    • 使用“字符串”构建器时,只会创建一个对象[StringBuilder是muttable],并且会向其附加另一个字符串 .

  • 217

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

    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
    

    问题是to =追加到一个字符串重建一个新的字符串,所以它花费了与字符串长度成线性的东西(两者的总和) .

    所以 - 对你的问题:

    第二种方法会更快,但它的可读性和维护难度更低 . 正如我所说,在你的具体情况下,你可能不会看到差异 .

  • 3

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

    因此,为了代码可读性,请使用“” .

    2例外:

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

    出于性能原因,不鼓励使用 +=String 连接) . 原因是:Java String 是一个不可变的,每次创建新的连接时都会创建一个新的 String (新的指纹与旧的指纹已经in the String pool不同) . 创建新字符串会给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) .

  • 1

    尽可能使toString方法可读!

    在我的书中唯一的例外是,如果你能向我证明它消耗了大量资源:)(是的,这意味着分析)

    另请注意,Java 5编译器生成的代码比早期Java版本中使用的手写“StringBuffer”方法更快 . 如果你使用“”这个和未来的增强功能是免费的 .

  • 1

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

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

    按顺序,我会说构造字符串的首选方法是使用StringBuilder,String#concat(),然后是重载的运算符 . StringBuilder在使用大型字符串时显着提升性能,就像使用运算符一样,性能大幅下降(随着字符串大小的增加,指数大幅减少) . 使用.concat()的一个问题是它可以抛出NullPointerExceptions .

  • 4

    在Java 9中,版本1应该更快,因为它被转换为 invokedynamic 调用 . 更多细节可以在JEP-280找到:

    我们的想法是用对java.lang.invoke.StringConcatFactory的简单invokedynamic调用替换整个StringBuilder追加舞蹈,它将接受需要连接的值 .

  • 846

    我还与我的老板发生冲突,关于是否使用append或 . 他们正在使用Append(我仍然无法弄清楚他们每次创建新对象时都会说出来) . 所以我想做一些R&D . 虽然我喜欢Michael Borgwardt的解释,但只想展示一个解释,如果有人将来真的需要知道 .

    /**
     *
     * @author Perilbrain
     */
    public class Appc {
        public Appc() {
            String x = "no name";
            x += "I have Added a name" + "We May need few more names" + Appc.this;
            x.concat(x);
            // x+=x.toString(); --It creates new StringBuilder object before concatenation so avoid if possible
            //System.out.println(x);
        }
    
        public void Sb() {
            StringBuilder sbb = new StringBuilder("no name");
            sbb.append("I have Added a name");
            sbb.append("We May need few more names");
            sbb.append(Appc.this);
            sbb.append(sbb.toString());
            // System.out.println(sbb.toString());
        }
    }
    

    并且上面的类的反汇编出来了

    .method public <init>()V //public Appc()
      .limit stack 2
      .limit locals 2
    met001_begin:                                  ; DATA XREF: met001_slot000i
      .line 12
        aload_0 ; met001_slot000
        invokespecial java/lang/Object.<init>()V
      .line 13
        ldc "no name"
        astore_1 ; met001_slot001
      .line 14
    
    met001_7:                                      ; DATA XREF: met001_slot001i
        new java/lang/StringBuilder //1st object of SB
        dup
        invokespecial java/lang/StringBuilder.<init>()V
        aload_1 ; met001_slot001
        invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
    g/StringBuilder;
        ldc "I have Added a nameWe May need few more names"
        invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
    g/StringBuilder;
        aload_0 ; met001_slot000
        invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan\
    g/StringBuilder;
        invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;
        astore_1 ; met001_slot001
      .line 15
        aload_1 ; met001_slot001
        aload_1 ; met001_slot001
        invokevirtual java/lang/String.concat(Ljava/lang/String;)Ljava/lang/Strin\
    g;
        pop
      .line 18
        return //no more SB created
    met001_end:                                    ; DATA XREF: met001_slot000i ...
    
    ; ===========================================================================
    
    ;met001_slot000                                ; DATA XREF: <init>r ...
        .var 0 is this LAppc; from met001_begin to met001_end
    ;met001_slot001                                ; DATA XREF: <init>+6w ...
        .var 1 is x Ljava/lang/String; from met001_7 to met001_end
      .end method
    ;44-1=44
    ; ---------------------------------------------------------------------------
    
    
    ; Segment type: Pure code
      .method public Sb()V //public void Sb
      .limit stack 3
      .limit locals 2
    met002_begin:                                  ; DATA XREF: met002_slot000i
      .line 21
        new java/lang/StringBuilder
        dup
        ldc "no name"
        invokespecial java/lang/StringBuilder.<init>(Ljava/lang/String;)V
        astore_1 ; met002_slot001
      .line 22
    
    met002_10:                                     ; DATA XREF: met002_slot001i
        aload_1 ; met002_slot001
        ldc "I have Added a name"
        invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
    g/StringBuilder;
        pop
      .line 23
        aload_1 ; met002_slot001
        ldc "We May need few more names"
        invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
    g/StringBuilder;
        pop
      .line 24
        aload_1 ; met002_slot001
        aload_0 ; met002_slot000
        invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan\
    g/StringBuilder;
        pop
      .line 25
        aload_1 ; met002_slot001
        aload_1 ; met002_slot001
        invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;
        invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
    g/StringBuilder;
        pop
      .line 28
        return
    met002_end:                                    ; DATA XREF: met002_slot000i ...
    
    
    ;met002_slot000                                ; DATA XREF: Sb+25r
        .var 0 is this LAppc; from met002_begin to met002_end
    ;met002_slot001                                ; DATA XREF: Sb+9w ...
        .var 1 is sbb Ljava/lang/StringBuilder; from met002_10 to met002_end
      .end method
    ;96-49=48
    ; ---------------------------------------------------------------------------
    

    从上面两个代码可以看出迈克尔是对的 . 在每种情况下,只创建一个SB对象 .

  • 63

    版本1更可取,因为它更短,而且没有任何性能差异 .

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

    通常在编译器无法自行替换 StringBuilder 时're concatenating in a loop - that' s .

相关问题