ページ

2014年4月1日

Java8のString::join

実際のコードを書きたかったのですが、すぐに思い出せないので、わずかながらの記憶で書きます。java8を使わない人も楽しめるはず。

SQL文を組み立てるときに、時々こんな感じのコードを見かけます。

コード1 ::
StringBuilder sb = new StringBuilder()
boolean isFirst = true;
for (int i=0; i<10; i++) {
    if (!isFirst) {
        sb.append(",");
    }
    sb.append("?");
}

もしくは、

コード2::
sb.append("column1");
sb.append(",");
sb.append("column2");
sb.append(",");
sb.append("column3");

ちょっと例が悪いですが、多分、こんな感じのコードにであったことはあると思います。アリエルのコードの場合、StringUtilsと言うクラスがいて、joinメソッドがあります。public static String StringUitls.join( Collection<String> elems, String delimiter)みたない感じです。

コード1のようなコードは、

String s = StringUtils.join(StringUtils.multiply("?", 10), ",")

って書けます。Javaは演算子のオーバーロードができないので、multipyってメソッドで*相当のことをしています。このへんはPythonの影響を受けているので、そっちを見るのがいいのかも。

コード2は

String s = StringUtils.join(new String[]{"column1", "column2", "column3"}, ",");

みたいな感じです。
アリエルの人たちは、「Stringにjoinがなくてめんどくさいな〜。でも、おれらはStringUtilsがあるからかんけーないね。」って思っていました。

さてさて、みんなが待ちに待ったJava8は華やかなlambdaやstream api、ついでにCalendar API(古いやつがスレッドセーフじゃないってどういうことじゃ!)も仲間に入れてあげるとして、そんな今どきの人に隠れてStringクラスの地味な拡張がjoinメソッドの追加です。アリエルはすぐにjava8に対応するので、これで十数行、全体のコード行が短くなるはず。えー、誤差の範囲です…。

で、Stringクラスにjoinメソッドが追加されて、コード1,2は次のようになります。

String s = String.join("," StringUtils.multiply("?", 10))
String s = String.join(", ", new String[]{"column1", "column2", "column3"});

スッキリ。
さて、Sting::joinはどうやって作っているのでしょうか?ちょっとだけコードを覗きます。3月11日のmercurialのリポジトリのコードです。

    public static String join(CharSequence delimiter,
            Iterable<? extends CharSequence> elements) {
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        StringJoiner joiner = new StringJoiner(delimiter);  // ①
        for (CharSequence cs: elements) {
            joiner.add(cs);  // ②
        }
        return joiner.toString(); // ③
    }

StringJoinerという新しいクラスが登場しています(①)。このクラスは、addした文字列(②)をdelemiterで連結してくれるクラスですね。最期に、文字列の出力です(③)。アリエルのStringUtils.joinはStringBuilderで文字列をその場で組み立てていました。ちょうど、コード1を汎用化した感じですね。


StringJoinerはコンストラクタが2つあって、
  StringJoiner(delimiter)
  StringJoiner(delemiter, prefix, suffix)

こんな感じで使えますね

StringJoiner sj = new StringJoiner(", ");
sj.add("column1");
sj.add("column2");
sj.add("column3");
sj.toString();   // column1, column2, column3になる

SQL文の組み立てだと最初と最期に( )が追加したいので、そういう時は、

StringJoiner sj = new StringJoiner(", ", "(", ")");
sj.add("column1");
sj.add("column2");
sj.add("column3");
sj.toString();   // (column1, column2, column3)になる

ほら、べんり。StingJoinerの実装でも、内部的なデータの保持はStringBuilderのオブジェクトを作っていて、その人にappendをしているだけですが、APIとしてはスッキリしていて、クラスの目的も明確ですね。





次のコードはStringJoinerのStringBuilderを取得しているコードです。valueはStringBuilderのクラスフィールドです。コード1だとisFirstというbooleanの値を見て、delemiterを追加するかどうか決めていましたが、value自体をフラグの代わりに使って、こんな風に書くとそういうフラグはいらないんですね。

  private StringBuilder prepareBuilder() {
        if (value != null) {
            value.append(delimiter);
        } else {
            value = new StringBuilder().append(prefix);
        }
        return value;
    }

それから、状況によっては、value.length()>0でdelemiterを追加するかどうかを決めるというのもできますね。


「私は、Stream APIとlambdaがあれば十分よ。String::joinなんていらない子よ」って。ごめんなさい。そういう人はそっちを使ってあげてください。

0 件のコメント: