移転しました。

String VS StringBuffer

前回のStringの呼び出しコストに続き、今回は文字列結合に最適な方法について決着をつけようと思います。


検証に使ったコードは以下のとおりです。

public class ConcatString {
    private static int testCount;
    private static StringBuffer buffer;

    public static void test(int count) {
        testCount = count;
        buffer = new StringBuffer(testCount * 10);
        TEST_END:
        for (int i = 0; ; i++) {
            buffer.delete(0,buffer.length());
            long start = System.currentTimeMillis();
            switch (i) {
            case 0:
                System.out.println("concatLiteral()");
                concatLiteral();
                break;
            case 1:
                System.out.println("concatMethod()");
                concatMethod();
                break;
            case 2:
                System.out.println("appendMethod()");
                appendMethod();
                break;
            case 3:
                System.out.println("appendDirectConcatLiteral()");
                appendDirectConcatLiteral();
                break;
            case 4:
                System.out.println("appendPreConcat1Litera()");
                appendPreConcat1Litera();
                break;
            case 5:
                System.out.println("appendAppendMethod()");
                appendAppendMethod();
                break;
            case 6:
                System.out.println("concatString()");
                concatString();
                break;
            default:
                break TEST_END;
            }
            long end = System.currentTimeMillis();
            double result = (double)(end - start) / 1000;
            System.out.println(result + "秒");
        }
    }
    protected static void concatLiteral() {
        for (int i = 0; i < testCount; i++) {
            String s = "ABC" + "ABC";
        }
    }
    protected static void concatMethod() {
        for (int i = 0; i < testCount; i++) {
            String s = "ABC".concat("ABC");
        }
    }
    protected static void appendMethod() {
        for (int i = 0; i < testCount; i++) {
            buffer.append("ABC");
        }
    }
    protected static void appendDirectConcatLiteral() {
        for (int i = 0; i < testCount; i++) {
            buffer.append("ABC" + "ABC");
        }
    }
    protected static void appendPreConcat1Litera() {
        for (int i = 0; i < testCount; i++) {
            String s = "ABC" + "ABC";
            buffer.append(s);
        }
    }
    protected static void appendAppendMethod() {
        for (int i = 0; i < testCount; i++) {
            buffer.append("ABC");
            buffer.append("ABC");
        }
    }
    protected static void concatString() {
        for (int i = 0; i < testCount; i++) {
            String s = "ABC";
            s += "ABC";
        }
    }
    public static void main(String[] args) {
        ConcatString.test(5000000);
    }
}

この検証コードはVMにある程度メモリを渡さないとOutOfMemoryErrorになると思いますので実行する際には注意して下さい。


一応、比較しやすいように実行時の結果も載せておきます。

concatLiteral()
0.015秒
concatMethod()
1.235秒
appendMethod()
0.515秒
appendDirectConcatLiteral()
0.516秒
appendPreConcat1Litera()
0.516秒
appendAppendMethod()
1.015秒
concatString()
2.719秒


concatLiteral()
String変数に値を設定する際にリテラルを+演算子によって結合したものです。これはコンパイラ

String s = "ABCABC";

に書き換えるので、文字列結合自体のオーバーヘッドがなく高速です。


concatMethod()
+演算子ではなくconcatメソッドで結合しました。これはコンパイラによって自動で一つの文字列になることはありませんのでかなり低速です。Stringが変更不可なオブジェクトであることを考えると再生性するコストがかかっていることが予想できます。


appendMethod()
StringBufferによる文字列結合です。バッファ値を再設定するこのとないよう初期値で設定しているので純粋なappendメソッドの検証となりますが、concatメソッドの半分程度のコストしかかかりませんでした。


appendDirectConcatLiteral()
appendメソッド内にString結合があるとどうなるかというものです。この場合はString結合にStringBufferが使用され一つ余分なStringBufferが生成されてしまうと言われますが、ベンチでの結果は全く同じでしたので、速度面では気にする必要はなさそうです。但し、余分なメモリを消費するかについては検証できていませんので、このようなコーディングは避けた方がよいのかもしれません。


appendPreConcat1Litera()
appendメソッド内ではなく前もって結合した文字列を用意してあげるとどうかです。これも速度面での差はなく、単純にappendした際と同じ結果でした。


appendAppendMethod()
StringBufferにさらに追加した場合です。これは単純に倍のコストが掛かったのでバッファの再設定がなければappendメソッドを呼び出すオーバーヘッドのみとなるようです。


concatString()
最後にStringを+=演算子を用いて結合する場合ですが、これは非常に低速でした。concatメソッドのさらに倍程度のコストが掛かっています。+=演算子でなければ実現しないような場合以外は避けた方がよさそうです。


結果としては、文字列が可変でない場合は潔くStringを+演算子で繋いで一つの文字列にしてしまうのが一番良さそうです。文字列が可変の場合はStringBufferを利用する方が高速ですね。検証コードではバッファ値を初期設定で指定していますが、指定しなかった場合でもStringのconcatメソッドで結合するよりは高速でしたので、結果として生成される文字列のサイズがわからなくてもStringBufferを利用する価値はあります。StringBuffer自体の生成コストやtoString()時のコストは無視できるレベルだと思います。

ということで一般的な論が大幅正解というところでしょうか。ただ、見落としがちなのは単純結合の場合はStringBufferを利用するより確実に低コストな点です。比較してみます。

String query = ""
    + "SELECT "
    +     "name "
    + "FROM "
    +     "human";
StringBuffer query = new StringBuffer();
query.append("SELECT "  );
query.append(    "name ");
query.append("FROM "    );
query.append(    "human");

コスト面では前者が優位です。但しコーディング規約で後者で統一するのも一つだと思いますので、中身の挙動を知って選択することが大切だと思います。