9.63: 文字列結合のパフォーマンス
結論
文字列結合は 状況に応じて手段を選ぶ。
少量・単純な結合は + で可読性優先で良いが、ループ内や大量の連結、集合の結合では StringBuilder / String.join / Collectors.joining を使って中間オブジェクトの大量生成を避け、性能とメモリ効率を確保する。
ログではパラメータ化を使い、String.format は便利だがコストが高いことに注意する。
良い例
- ループ外で
StringBuilderを作る(ホットパスでの定番)
public String joinNames(List<String> names) {
StringBuilder sb = new StringBuilder(names.size() * 16); // 初期容量を見積もる
for (String name : names) {
if (sb.length() > 0) sb.append(", ");
sb.append(name);
}
return sb.toString();
}
- コレクションを区切りで結合するなら標準 API を使う(簡潔で高速)
String csv = String.join(", ", names);
// または Stream API
String csv2 = names.stream().collect(Collectors.joining(", "));
- ログはフォーマット済文字列を直接作らない(スレッドレベルが低い場合でも無駄な生成を避ける)
// SLF4J など
logger.debug("Processed {} records in {}ms", count, elapsed);
悪い例
- ループ内で
+=(+)を使うと中間Stringが毎回生成される(O(n^2) 的なコストになり得る)
public String badJoin(List<String> names) {
String s = "";
for (String name : names) {
if (!s.isEmpty()) s += ", "; // 毎回新しい String を作る => 非常に遅い/メモリ浪費
s += name;
}
return s;
}
- String.format をホットパスで多用する(可読だが高コスト)。
for (Item it : items) {
out.println(String.format("%s: %d", it.getName(), it.getCount())); // かなり重い
}
まとめ
-
小さな、コードが読みやすい単発の結合は
+を使って問題ない -
ループで繰り返し結合する場合は 必ず
StringBuilderを外で作る(初期容量を見積もると良い) -
複数要素を区切り付きで結合するなら
String.join/Collectors.joiningを使う -
String.formatは可読性が必要なケースで使うが、ホットパスや大量生成では避ける -
スレッド安全を理由に同期された
StringBufferを選ぶ必要はほとんどない(必要なら別途同期設計を検討) -
ログはパラメータ化(
logger.debug("x={}", x))で不要な文字列生成を防ぐ -
性能が疑わしいときは 計測(プロファイラ/JMH) をしてから最適化する