Java8時代の文字列連結まとめ

文字列の配列やリストを[〜]で囲ってカンマで区切って連結するという話、String.joinだとどう?とwatermintさんから指摘があったので、試してみました。
シンプル!

    public static String stringJoin(){
        return "[" + String.join("],[", strarray) + "]";
    }

でも、1847msでした。改めて前後の文字を文字列連結してるところで時間かかってる感じ。


で、昨日のStringBuilder版はもう少し最適化できるので書き直します。

    public static String stringBuilderJoin(){
        StringBuilder s = new StringBuilder("[");
        for(int i = 0; i < strarray.length; ++i){
            if(i != 0){
                s.append("],[");
            }
            s.append(strarray[i]);
        }
        s.append("]");
        return s.toString();
    }

そうすると1177ms。すげー速いわ。
昨日のままだと1500msでしたね。まだまだでした・・・。
あと12:37追記で。
コードをさらに最適化したものをcolunさんに教えてもらって試したところ、10ms程度遅くなりました。これも意外。
http://ideone.com/9OzhAe
追記ここまで。


ちなみに、昨日のクソコード版。

    public static String stringBuilderFuckingJoin(){
        StringBuilder s = new StringBuilder();
        for(int i = 0; i < strarray.length; ++i){
            if(i != 0){
                s.append(",");
            }
            s.append("[" + strarray[i] + "]");
        }
        return s.toString();
    }    

1632ms。
append回数で速度がかなり違うんで、パフォーマンスが気になる部分ではやはりコードの書き方は気を使ったほうがいいですね。


で、これ書きながら、そういえばどこかにprefix/suffix指定できるjoinがあったよなーと思ったら、Collectors.joiningにありました。昨日気づいてれば・・・
まあ、とりあえず書いてみます。
だいぶシンプル!

    public static String streamListJoin3(){
        return strlist.stream()
                .collect(Collectors.joining("],[", "[", "]"));
    }

1583ms!
昨日のStringBuilder版と同じくらいだけど、最適化したStringBuilder版とはかなり差が。
でも前回は4720msだったのが、連結回数が減った分そのまま速くなった感じ。


こうなると、並列化に期待したくなりますね。

    public static String streamListParallelJoin3(){
        return strlist.parallelStream()
                .collect(Collectors.joining("],[", "[", "]"));
    }

1755ms・・・
遅くなってます。並列化、逆効果ですね。
結局文字列連結ではメモリ確保に時間がかかるので、並列化してもあまり意味がなく逆効果ってことなのかも。
残念です。


で、String.joinが前後の文字列を付け加える処理で遅そうだったので、内部で使ってるjava.util.StringJoinerを直接使ってみます。これは、コンストラクタでprefix/surfixが指定できます。

    public static String stringJoiner(){
        StringJoiner sj = new StringJoiner("],[", "[", "]");
        for(int i = 0; i < strarray.length; ++i){
            sj.add(strarray[i]);
        }
        return sj.toString();
    }

1430ms。
速くなったけど、StringBuilderを直接使うのと差がかなりありますね。
ソース見る限りでは、違いはメソッド呼び出しの深さと、判定がintの比較かオブジェクトのnull比較かだけだと思うんですが。
※19:21追記
おそらくStringBuilder版がループ中のifを外に出す最適化が行われてcolunさんのコードと等価になって速くなっていたものが、StringJoinerではメソッドで分断されるために最適化が効かなくて遅くなっているんではないかと思われます。
追記ここまで。


あと少し気になったので、StringBuilderであらかじめメモリを確保して試してみました。

    public static String stringBuilderJoinMem(){
        StringBuilder s = new StringBuilder(9000).append("[");
        for(int i = 0; i < strarray.length; ++i){
            if(i != 0){
                s.append("],[");
            }
            s.append(strarray[i]);
        }
        s.append("]");
        return s.toString();
    }

1071ms!すごく速い!
速度こだわるなら、メモリちゃんと確保したStringBuilderですねぇ・・・
ちょっと、「Java8からはStringBuilderよりStreamやで!」とか「StringJoiner使おう」とか無条件には言えそうにない。


まとめるとこんな感じの結果に。

stringJoin:1847ms
stringJoiner:1430ms
streamListJoin3:1583ms
streamListParallelJoin3:1755ms
stringBuilderJoin:1177ms
stringBuilderJoinMem:1071ms
stringBuilderFuckingJoin:1632ms


ということで、Streamの並列の夢が打ち砕かれたってことで。


あと、ググれば出てくるような内容でも、自分にとって発見ならブログ書いたほうがいいと思いますね。
ブログ書かないときよりちゃんと調べるし、ちゃんと試すし。
シチュエーションまで他の記事と同じということはなくて、ちょっとした差でちょっとした価値がある記事になったりもするし。特に、今回のようにJavaの新しいバージョンが出たタイミングでは、今までと常識が変わる可能性もあるので、これまでどおりであることを確認するのはそれはそれで大事です。今回は新しい書き方も発見できたし。


それに、書くことで、次の発想につながるわけだし。書かずに納得してるだけだと、そこで終わりだったり、そこで終わりならまだよくて、同じことモンモンと考えてたりしますね。書いて脳みそのスペースあけることも大事。
まあ、他人の記事のコピペとかじゃなければ、どっかに書いてあったよねとか気にせず、意識を低く持ってどんどんブログ書けばいいんじゃないでしょうか。個人の日記だし。


今回分のコードはこれ

public class StringJoinBench {
    static String[] strarray;
    static List<String> strlist;
    
    public static void main(String[] args) {
        strarray = IntStream.range(0, 1000).map(i -> (i % 9) + 1)
                .mapToObj(len -> IntStream.range(0, len).mapToObj(i -> "a").collect(Collectors.joining()))
                .toArray(size -> new String[size]);
        strlist = Arrays.asList(strarray);
        
        bench("stringJoin", StringJoinBench::stringJoin);
        bench("stringJoiner", StringJoinBench::stringJoiner);
        bench("streamListJoin3", StringJoinBench::streamListJoin3);
        bench("streamListParallelJoin3", StringJoinBench::streamListParallelJoin3);
        bench("stringBuilderJoin", StringJoinBench::stringBuilderJoin);
        bench("stringBuilderJoinMem", StringJoinBench::stringBuilderJoinMem);
        bench("stringBuilderFuckingJoin", StringJoinBench::stringBuilderFuckingJoin);
    }
    public static String stringJoin(){
        return "[" + String.join("],[", strarray) + "]";
    }
    public static String stringJoiner(){
        StringJoiner sj = new StringJoiner("],[", "[", "]");
        for(int i = 0; i < strarray.length; ++i){
            sj.add(strarray[i]);
        }
        return sj.toString();
    }

    public static String stringBuilderJoin(){
        StringBuilder s = new StringBuilder("[");
        for(int i = 0; i < strarray.length; ++i){
            if(i != 0){
                s.append("],[");
            }
            s.append(strarray[i]);
        }
        s.append("]");
        return s.toString();
    }
    public static String stringBuilderJoinMem(){
        StringBuilder s = new StringBuilder(9000).append("[");
        for(int i = 0; i < strarray.length; ++i){
            if(i != 0){
                s.append("],[");
            }
            s.append(strarray[i]);
        }
        s.append("]");
        return s.toString();
    }

    public static String stringBuilderFuckingJoin(){
        StringBuilder s = new StringBuilder();
        for(int i = 0; i < strarray.length; ++i){
            if(i != 0){
                s.append(",");
            }
            s.append("[" + strarray[i] + "]");
        }
        return s.toString();
    }
    public static String streamListJoin3(){
        return strlist.stream()
                .collect(Collectors.joining("],[", "[", "]"));
    }
    public static String streamListParallelJoin3(){
        return strlist.parallelStream()
                .collect(Collectors.joining("],[", "[", "]"));
    }

    public static void bench(String name, Supplier<String> proc){
        bench(name, 50_000, proc);
    }
    public static void bench(String name, int count, Supplier<String> proc){
        if(!stringJoin().equals(proc.get())) throw new RuntimeException(name + " is defferent with array join.");
        for(int i = 0; i < 100; ++i){
            proc.get();
        }
        long s = System.currentTimeMillis();
        for(int i = 0; i < count; ++i){
            proc.get();
        }
        System.out.printf("%s:%dms%n", name, System.currentTimeMillis() - s);
    }
}