web-dev-qa-db-ja.com

JavaにおけるtoString()のStringBuilderと文字列の連結

以下の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に切り替えますか?

845
non sequitor

バージョン1はより短く、そして コンパイラは実際にそれをバージョン2に変えるでしょう - パフォーマンスの違いは全くないので、バージョン1が好ましいです。

さらに重要なことに、私たちには3つのプロパティしかないので、違いはないかもしれませんが、どの時点でconcatからbuilderに切り替えますか?

ループ内で連結している時点で、通常はコンパイラがStringBuilderを単独で代用することはできません。

880

重要なのは、1つの場所に単一の連結を作成するのか、それとも時間の経過とともにそれを蓄積するのかということです。

あなたが挙げた例では、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();
225
joel.neely

私が好む:

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

...それは短くて読みやすいからです。

私はnot非常に高い繰り返しカウントおよびパフォーマンスの違いを測定しました。

多くのパラメータを出力する必要がある場合、このフォームは混乱する可能性があります(コメントの1つが言うように)。この場合、より読みやすい形式に切り替えて(おそらく、Apache-commonsの ToStringBuilder を使用-matt bの回答から取得)、パフォーマンスを再度無視します。

71
tangens

ほとんどの場合、2つの方法の実際の違いはわかりませんが、次のような最悪のシナリオを簡単に構築できます。

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

問題は、文字列に+ =を追加すると新しい文字列が再構築されるため、文字列の長さ(両方の合計)に比例したコストがかかることです。

だから - あなたの質問に:

2番目の方法はより高速になりますが、読みにくくなり、保守が難しくなります。私が言ったように、あなたの特定のケースではあなたはおそらく違いを見ないでしょう。

63
Omry Yadan

私は上司とappendを使うべきか+を使うべきかどうかについても衝突しました。私はマイケル・ボルグワードの説明が大好きですが、将来誰かが本当に知る必要があるならば説明を見せたいだけでした。

/**
 *
 * @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
; ---------------------------------------------------------------------------

上記の2つのコードから、Michaelが正しいことがわかります。いずれの場合も、SBオブジェクトは1つだけ作成されます。

27
perilbrain

Java 1.5以降、 "+"とStringBuilder.append()を使用した単純な1行連結で、まったく同じバイトコードが生成されます。

コードを読みやすくするために、 "+"を使用してください。

2つの例外

  • マルチスレッド環境:StringBuffer
  • ループ内の連結:StringBuilder/StringBuffer
24
Zofren

最新バージョンの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:(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

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

明白な違い が少しあります。 +が使用された最初のケ​​ースでは、ループ反復ごとに新しいStringBuilderが作成され、生成された結果はtoString()呼び出しを行うことによって格納されます(29から41)。そのため、forループで+演算子を使用するときに、実際には必要としない中間文字列を生成しています。

21
ring bearer

Java 9では、バージョン1はinvokedynamic呼び出しに変換されるため、高速になるはずです。詳細は JEP-280 にあります。

アイデアは、StringBuilderのappend dance全体をJava.lang.invoke.StringConcatFactoryへの単純なinvokeynamic呼び出しに置き換えることです。これは連結を必要とする値を受け入れます。

8
ZhekaKozlov

パフォーマンス上の理由から、+=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)。

7
Paolo Maresca

Apache Commons-Langには ToStringBuilder クラスがあり、これは非常に使いやすいです。それは、append-logicを扱うことと、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のスタイルをカスタマイズすることもできます。

7
matt b

ToStringメソッドをできるだけ読みやすくします。

私の本の中でこれに対する唯一の例外は、あなたが 証明することができれば はそれがかなりのリソースを消費することを私に言っている:)(はい、これはプロファイリングを意味します)

また、Java 5コンパイラは、以前のバージョンのJavaで使用されていた手書きの "StringBuffer"アプローチよりも高速なコードを生成します。あなたが "+"を使うなら、これと将来の機能強化は無料で来る。

StringBuilderを使用することが現在のコンパイラでまだ必要とされているかどうかいくつかの議論があるようです。だから私は2セントの経験をするつもりだと思った。

私はJDBCという10kレコードの結果セットを持っています(そう、それらすべてを1つのバッチにまとめる必要があります)。私のマシンではJava 1.8を使って+演算子を使うのに約5分かかります。 stringBuilder.append("")を使用すると、同じクエリに1秒もかかりません。

だから違いは大きいです。ループ内ではStringBuilderがずっと速くなります。

3
Eddy

文字列はJavaでは不変であるため、 '+'を使用した文字列の連結でのパフォーマンスは、まったく新しいStringのコピーを作成する必要があるため、コストがかかります。これは、連結が非常に頻繁に行われる場合(ループ内など)に特に役立ちます。以下は、私がそのようなことをやろうとしたときに私のIDEAが示唆するものです。

enter image description here

一般的なルール:

  • 単一の文字列割り当ての中では、文字列連結を使用することは問題ありません。
  • 文字データの大きなブロックを作成するためにループしている場合は、StringBufferを使用してください。
  • Stringに+ =を使用するとStringBufferを使用するよりも常に効率が悪くなるため、警告ベルを鳴らす必要があります。ただし、場合によっては、読みやすさの問題と比較して最適化が無視できる程度になります。

これは このトピックに関する/ - Nice Jon Skeetブログ です。

2
Sudip Bhandari

以下の例を見てください。

//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ミリ秒平均
作成された文字列の長さ:1950000 in 17745 ms
10000連結の平均時間:平均0.21ミリ秒
10000連結の平均時間:0.652ミリ秒平均
連結10000の平均時間:平均1.129ミリ秒
10000連結の平均時間:1.727ミリ秒平均
10000連結の平均時間:2.302ミリ秒平均
作成された文字列の長さ:1950000 in 60279 ms
連結10000の平均時間:平均0.002ミリ秒
連結10000の平均時間:平均0.002ミリ秒
連結10000の平均時間:平均0.002ミリ秒
連結10000の平均時間:平均0.002ミリ秒
連結10000の平均時間:平均0.002ミリ秒
作成された文字列の長さ:1950000 in 100 ms

文字列の長さが長くなるにつれて、連結時間も長くなります。
それがStringBuilderが確実に必要とされるところです。
ご覧のとおり、連結UUID.randomUUID()+"---"は実際には時間に影響を与えません。

P.S .: とは思わない JavaでStringBuilderを使用する場合 は本当にこれと同じです.
この質問は、ほとんどの場合巨大な文字列の連結を行わないtoString()について話しています。

2
Marinos An

コレクションを反復処理してStringBuilderを使用する場合は、 Apache Commons Lang および StringUtils.join() (さまざまな種類の)をチェックアウトすることをお勧めします。

パフォーマンスに関係なく、 millionth timeのように見えるものに対してStringBuildersとforループを作成する必要がなくなります。

1
Brian Agnew

パフォーマンスを比較するために4つの異なるアプローチを比較しました。私は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";
}
}
1
Hamedz

ここに私がJava8でチェックしたものがあります

  • 文字列連結の使用
  • StringBuilderを使用する

    long time1 = System.currentTimeMillis();
    usingStringConcatenation(100000);
    System.out.println("usingStringConcatenation " + (System.currentTimeMillis() - time1) + " ms");
    
    time1 = System.currentTimeMillis();
    usingStringBuilder(100000);
    System.out.println("usingStringBuilder " + (System.currentTimeMillis() - time1) + " ms");
    
    
    private static void usingStringBuilder(int n)
    {
        StringBuilder str = new StringBuilder();
        for(int i=0;i<n;i++)
            str.append("myBigString");    
    }
    
    private static void usingStringConcatenation(int n)
    {
        String str = "";
        for(int i=0;i<n;i++)
            str+="myBigString";
    }
    

多数の文字列に文字列連結を使用している場合、それは本当に悪夢です。

usingStringConcatenation 29321 ms
usingStringBuilder 2 ms
0
nagendra547