読者です 読者をやめる 読者になる 読者になる

日々常々

ふつうのプログラマがあたりまえにしたいこと。

文字列連結と+演算子について整理しておく

Java Java入門+α

何度か書いているけど、整理的な意味で。今後は「このエントリ参照」にするつもりで書いてみる。

前書き

Stringなんてboxed primitive*1でもないただのクラスのくせに、中途半端に贔屓されて*2てムカつく*3し、その中途半端ぶり*4がなお腹立たしい……。そして +演算子 で連結して問題が起こるような状況、つまりそんな長々と文字列連結したいような場合は、きっと他の適した型がある。StringBuilderじゃなく、もっと別の何か。業務要件で文字列を組み立てる目的を考えれば、たぶんテンプレート的なものに落ち着くんじゃ無かろうか。ライブラリ的な所でなら逐次書き出し等になるような。どちらにせよ文字列の組み立ては隠蔽されるはずだし、そう考えると +演算子 云々なんてぶっちゃけどうでも良いことに……。


個人的な感情はさておき、定期的に話題になる「Stringの+演算子による連結」について整理しておきます。

+演算子 を使用するべき状況がある

まず事実として。
知っている人にとっては当然のことですが、+演算子 の方が良い場合があります。その条件は +演算子 の両辺が定数の場合。

String str = "abc" + "def";

これは "abcdef" と、最初から連結した状態にコンパイルされます。実行時はそのまま "abcdef" が使用されるため、これ以上高速なものはありません。ここで StringBuilder を使用するのは明らかにおかしいです。
上記のコードではリテラル同士で連結していますが、定数*5であれば同様です。

final String s1 = "abc";
String s2 = s1 + "def";

s2 には "abcdef" が入るのは先のと同様。さらにここ以降で s1 が使われてない場合、クラスファイルからは消し飛んだりする。この辺りの詳しい話は置いといて、一応直感に反する(?)例もあるので書いておく。こんなの。

class A { static final String a = "a"; }
class B { String b = A.a + "b"; }

前述の通り、a は定数なので展開され、Bの b は "ab" と言う文字列になります。その結果、コンパイル後の B には class A なんて影も形も出て来ません。つまり、コンパイル後に A を再コンパイルしても B をコンパイルしなければ、 B.b の値は変わりません。興味があるなら `javap -v B` するといいです。この辺りの話は下のエントリでも書いてました。


一方で、幾らリテラル同士であると自分がわかっていても、別の文になっていればコンパイル時には連結されません。

String s1 = "abc";
String s2 = s1 + "def"; // 実行時に StringBuilder で連結されるようにコンパイルされる

定数で長い文字列を組み立てざるを得ない場合*7、適度な長さで文字列を +演算子 で連結して折り返すことで、可読性を上げることが出来ます。定数を +演算子 で連結した場合は一つの文字列になりますので、これによる実行時のパフォーマンス劣化はありません。こんな時に下手にStringBuilderなどを使う方が遅くなりますし、読み難いしで残念なことになるだけです。

+演算子でもStringBuilderでもたいして変わらない状況もある

連結した文字列をそのままの状態で使用する場合です。
ループで処理していたとしても、例えばループ内で毎回ログに出力するとか、別のメソッドの引数になる場合とかがこれに該当します。

// +演算子
for (String s : list) {
    String str = "abc" + s + suffix;
    someMethod(str);
}

// StringBuilder
for (String s : list) {
    StringBuilder sb = new StringBuilder();
    sb.append("abc")
    sb.append(s);
    sb.append(suffix);
    someMethod(sb.toString());
}

この場合、上のように +演算子 を使用しても、下のように StringBuilder を使用しても差はありません。+演算子コンパイル後は StringBuilder になりますので、クラスファイルを見る限り、これを区別することは出来ないと思います。この場合、可読性において +演算子 の方に軍配が上がるのではないでしょうか。一時変数 str をインライン化する選択も出来ますし。
もっとも、固定の文字列を組み立てる場合は String.format を使う方が私は好きですが。純粋に出来上がる文字列自体の可読性が段違いになりますし。

String s1 = "abc " + hoge + " def " + fuga;
String s2 = String.format("abc %s def %s", hoge, fuga);

String の static method なので、冗長な感じになるのが若干残念ですが……。

+演算子を使用すべきでない状況

連結直後の String が要らない場合がこれです。例えばこんな、String の +演算子 の話がでると必ず出てくるようなコード。

String str = "";
for (String s : list) {
    str += s;
}
someMethod(str);

ループ内で String は不要の状況です。ループ内では StringBuilder を使用して連結するのですが、それが再度 str 変数に格納するために String になります。そして次のループでは新たに StringBuilder が作られ……となるため、ループごとに StringBuilder と String のインスタンスが生成されます。これは流石によろしくない。下の例ならば StringBuilder はnewしたタイミング、連結した文字列の String は toString の一つずつになります。

StringBuilder sb = new StringBuilder();
for (String s : list) {
    sb.append(s);
}
someMethod(sb.toString());

「+演算子は駄目!」と言うのはほぼ常にこの文脈で語られます。現状では明らかに駄目ですが、ここも何らかの拍子に最適化されたりしないものだろうか。しないかな。ないか。あるか。わからない。難しいんじゃないかな、と思いつつ、現状では明白なNGパターン。ただし +演算子 を使用すべきでないのはこれだけだと思います。
こんな状況はそれほど多く無いと思っていますが、出てきた時には普通に対応出来ていて欲しい所。でもこんなのが頻繁に出るなら、もっと見直すべきものがあると思うし、個人的にはこんなのは外部イテレーターではなく内部イテレーターでやりたくなったりとか、文字列連結なんかとは違うところで色々と考えたくなります。

脱線: 現実問題的な話

ループ内での文字列連結がパフォーマンスに深刻な影響を与えることは、最近はめっきり無くなっています。この手の問題は往々にしてループ内でのオートボクシングとも並べて語られたりもしていて、最近のJVMの進化およびハードウェアの性能向上により、目立った問題では無くなっていると思っています。それよりももっと目立つボトルネックがあり*8、ぶっちゃけ「どうでもいい問題」です。ですが常識的なことでもあり、理解しているかは一つの試金石になるのかも知れません。
多人数開発の場合は一律のルールを徹底した方が、プロジェクト全体としてはマシなことになる状況もなくはなくはないのかもしれなくはないです……どっち。実際、そう判断されているのでしょう。これまで見てきた幾つかのコーディング規約にて、判を押したように「文字列の連結には StringBuffer を使用する」と書いていたりすることからもわかります。StringBuffer と書いている辺りから時代を感じ取って頂けると幸いです。

まとめ

個人的には String なんてコンピュータよりは人間向けなものをコードに登場させるんだから、そのコードの可読性だけで倒します。パフォーマンスが気になるなら String なんてやめる。可読性は周辺のコードにも依存する*9ので、どれが良いかは状況次第としか言いようが無いです。なので「常に +演算子 で構わない」とも「StringBuilder を使用するべき」とも言えないものです。

とりあえず、この程度のことを知っていれば、文字列連結なんて些細な問題だと言えると思います。

*1:Integerとか

*2:+演算子を使えることや、リテラルが使えることなど

*3:とは言え、これらが出来なきゃ面倒で仕方ないんですけどね:p

*4:主に == が使えないところ

*5:final な String 変数。もしくはリテラルで初期化されている final な String フィールド。コンストラクタやイニシャライザで初期化されているものはNGです。

*6:このタイトルは無いなぁ……orz

*7:SQLを書く場合とか?

*8:だいたいネットワークとかDBとかIO絡みなんですが。

*9:周りと比べて異質なコードは、たとえそれが洗練された書き方であっても、異なるってだけでも読み辛くなるものですから。