Java
C#
tips

意外と知られてないStringBuilderに関する初歩的なTips【Java・C#】

More than 1 year has passed since last update.

StringBuilderのインスタンスを生かしながら初期化する方法

文字列結合を高速化するためにSringBuilderを使うけど、インスタンスを使い回したいなぁ、と思う事がままあるかと思います。

例えばこんな実装コードです。
(実際にはループ処理とかじゃなくて、ちょろっと編集して文字列化、ちょろっと編集して文字列化、、、というのを何回か行うというケースの方が現実的。だけどサンプルコードとして書くと長いのでループ処理で代用するよ!)

コード例(C#)
StringBuilder sb;

foreach( var x in data )
{
    sb = new StringBuilder();

    // なんか文字列編集する処理

    string text = sb.ToString();
}

コード例(Java)
StringBuilder sb;

for ( Object x : data ) {
    sb = new StringBuilder();

    // なんか文字列編集する処理

    String text = sb.toString();
}

要するに、編集文字列をクリアする目的でインスタンスを再生成する、というケースです。

良く見るコードだと思います。

ただ、編集処理自体は大して重たくなく、StringBuilderのインスタンス生成のコストの方が勝っちゃうようなケースだと、インスタンスを使い回したくなると思います。

StringBuilderのクリア方法

で、表題の「意外と知られていない」件ですが、StringBuilderはインスタンスを生かしたまま内部の文字バッファをクリアする(←若干語弊がある説明)ための手段が用意されています。

コード例(C#)
var sb = new StringBuilder(); 

foreach( var x in data )
{
    sb.Length = 0; // これで前回編集していた文字列がクリアされて、新しい編集が開始できる。

    // なんか文字列編集する処理

    string text = sb.ToString();
}

コード例(Java)
StringBuilder sb = new StringBuilder();

for ( Object x : data ) {
    sb.setLength(0);

    // なんか文字列編集する処理

    String text = sb.toString();
}

※速度を目指すなら大体の編集サイズを予想して初期バッファを指定した方が良いです。これを指定するだけでも結構変わるので。

※もう一つの手段として delete すると言う手段もありますけど、あのコードはぐちゃぐちゃして解り難いので、(C#なら)拡張メソッドとかで追加してシンプルに呼び出せるようにしないとあまり好きじゃないです。いちいち削除文字数を指定するというAPIがなんか好きになれない。。。

正直、最初にこの方法を知った時には「それよりStringBuilder.Clear()メソッドを用意しといてくれればよかったのに!」と思いました。

まぁ、Clearメソッド増えたんですけどね。

C#:
https://msdn.microsoft.com/ja-jp/library/system.text.stringbuilder.clear(v=vs.110).aspx

※バージョンが上がって内部実装が変わっていなければ、こいつらは内部で 「Length プロパティに 0 をセットする」 と言う処理をやっているだけです。

Java:
調べてみたけどJavaはclearメソッド増えてなかった。
Java9とかで増えないかな??

※かわりに、Java9では "+" 演算子のコンパイル結果がStringBuilderよりも高速な実装に置き換わる、と言う噂を聞きました。

補足説明@Java

Javaでは、(どこかの古いバージョンかららしいですが)文字列結合を行う "+" 演算子は、コンパイルするとStringBuilderを使ったバイトコードが生成されるようになっています。

ぼくはJavaに入ったのはJava7(Java8でラムダが追加されるよって言われてワクテカしてた時期)時代からなので、色々と調べるまでは全然知りませんでした。
他にも substring の内部実装が変わったりと、(C#より)歴史が長いだけあって色々と変化があったみたいで、その辺も調べてみると面白いですね。

プラス演算子で実装したコード
String a = "そうです、";
String b = "我々は";
String c = "賢いので。"
String text = a + b + c; // そうです、我々は賢いので。

コンパイル結果と等価なコード
String a = "そうです、";
String b = "我々は";
String c = "賢いので。"
String text = new StringBuilder().append( a ).append( b ).append( c ).toString(); // そうです、我々は賢いので。

※もちろん定数展開とかのコンパイラ最適化を一旦無視した話ね!!

注意事項とか、あとがきてきなもの。

StringBuilderを長寿にし過ぎない方が良い:

クリアできる手段があるとはいえ、そもそもStringBuilderは編集バッファであって、あまり生存期間を長くするべきではないですし、
なにより、このクリア手段を使った事で処理速度が何倍・何十倍にも爆速化するか、と言えばまぁ別にそんな事も無いです。

やりすぎ禁物。

あくまで「編集領域」、バッファだよね:

StringBuilderは編集途中のバッファ領域でしかない、と言うスタンスは変えるべきでなく、
インスタンスを再利用してクリアして使う、と言うのは、せいぜい「単一のpublicメソッドから始まる処理内で完結するレベル」とかで抑えておくのが良いと思います。
(ぼくは普段そんな感じの指針でやってます。GC内で生存期間が伸びすぎて昇格されても厄介ですしね。。。)

後重要な判断基準としては「new StringBuilder();」というコードがノイズとして大き過ぎないかどうか。
メインのロジックが短くシンプルなのに比べてこいつが目立ちすぎるのであれば、やはり「sb.Clear();」と書けた方がコードが直感的になって良いと思います。

Lengthプロパティに対して思う事:

あと、個人的には「文字列編集した結果として文字数を得るプロパティであるLengthに対して、値をセットできてしまう」というのがそもそも直感的でなく、これをクリア手段として提供しているのはどうなのかな、って思いました。
Lengthはあくまで読み取り専用で、クリア手段は別途Clearメソッドが手段として提供されるべきじゃないかな、って思ってます。
(C#は追加されたんで、Javaも追加されないかなー)

まぁ、ぶっちゃけ「文字列をがちゃがちゃ組み上げる」って事は最近殆どやらないから良いんですけどね。
(物凄い長いSQLを文字列がちゃがちゃやって組み上げてた時代とかならまだしも・・・)