Javaプログラミングにおいて、文字列の結合は最も頻繁に行われる操作の一つです。

一見すると単純な作業に思えますが、JavaのString型が不変(Immutable)であるという特性を理解していないと、意図せぬパフォーマンスの低下やメモリ消費の増大を招く可能性があります。

本記事では、演算子による結合から、StringBuilder、さらには最新のJavaで推奨されるString.joinやテキストブロックまで、現場で役立つ最適な手法と使い分けについて徹底的に解説します。

Javaにおける文字列結合の基本概念

Javaの文字列結合を正しく理解するためには、まずStringオブジェクトが不変であるという事実を押さえる必要があります。

一度作成されたStringインスタンスの内容を変更することはできません。

文字列を結合するたびに、実は内部では新しいStringオブジェクトが生成されています。

例えば、String s = "A" + "B"; というコードを実行すると、元の “A” や “B” とは別に、新しく “AB” というメモリ領域が確保されます。

少数の結合であれば問題になりませんが、ループ内での大量の結合を行うと、ガベージコレクション(GC)の負荷が高まり、アプリケーションのレスポンスに悪影響を及ぼすことがあります。

そのため、状況に応じた最適なクラスやメソッドを選択することが、Javaエンジニアにとって重要なスキルとなります。

「+」演算子による結合とその内部動作

最も直感的で利用頻度が高いのは、プラス演算子(+)を使用した結合です。

ソースコードの可読性が高く、定数同士の結合や、ログ出力用の簡単なメッセージ作成に適しています。

コンパイラによる最適化(Java 9以降)

現代のJava(特にJava 9以降)では、プラス演算子による結合はコンパイラによって高度に最適化されています。

以前のバージョンでは内部的に StringBuilder に変換されていましたが、現在は invokedynamic 命令と StringConcatFactory を使用した手法に切り替わっています。

これにより、実行時に最適な結合戦略が選択されるため、単純な一行での結合においては「+」演算子を使用してもパフォーマンス上の懸念はほとんどありません

Java
public class PlusOperatorExample {
    public static void main(String[] args) {
        String firstName = "Taro";
        String lastName = "Yamada";

        // プラス演算子による単純な結合
        String fullName = firstName + " " + lastName;

        System.out.println("結合結果: " + fullName);
    }
}
実行結果
結合結果: Taro Yamada

演算子を使用すべき場面

  • 結合する要素の数が固定されている場合
  • 1行で完結する短いメッセージの構築
  • デバッグログの出力
  • コードの可読性を最優先したい場合

大量結合の救世主:StringBuilderとStringBuffer

ループ処理の中で文字列を動的に組み立てる場合、プラス演算子を使い続けるのは危険です。

各ループで新しいオブジェクトが生成され続けるため、計算量がO(n^2)となり、処理時間が指数関数的に増大します。

StringBuilderのメリット

StringBuilder は、可変(Mutable)な文字列を扱うためのクラスです。

内部にバッファを持ち、既存のメモリ領域を拡張しながら文字列を追加していくため、新しいオブジェクトの生成を最小限に抑えることができます。

Java
public class StringBuilderExample {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();

        // ループ内での効率的な結合
        for (int i = 0; i < 5; i++) {
            sb.append("Item").append(i).append(" ");
        }

        String result = sb.toString();
        System.out.println("ビルド結果: " + result);
    }
}
実行結果
ビルド結果: Item0 Item1 Item2 Item3 Item4

StringBuilderとStringBufferの違い

どちらも同じような機能を持ちますが、最大の違いはスレッドセーフかどうかにあります。

StringBuilder

同期化されていないため高速。

シングルスレッド環境や、メソッド内のローカル変数として使用する場合に最適。

StringBuffer

メソッドが同期化されており、マルチスレッド環境でも安全。

ただし、同期化のオーバーヘッドがあるため、通常は StringBuilder が推奨されます。

Java 8以降で導入された便利な結合手法

Java 8以降では、特定の区切り文字(デリミタ)で文字列を連結するための非常に便利な方法が追加されました。

CSV形式のデータ作成や、リストの内容を表示する際に威力を発揮します。

String.joinメソッド

もっとも手軽なのが String.join 静的メソッドです。

第一引数に区切り文字を指定し、第二引数以降に結合したい要素を渡します。

Java
public class StringJoinExample {
    public static void main(String[] args) {
        // 可変引数による結合
        String joined = String.join(", ", "Apple", "Banana", "Orange");
        System.out.println("果物リスト: " + joined);

        // リスト(Iterable)の結合
        java.util.List<String> list = java.util.Arrays.asList("Java", "Python", "Go");
        String lang = String.join(" | ", list);
        System.out.println("言語リスト: " + lang);
    }
}
実行結果
果物リスト: Apple, Banana, Orange
言語リスト: Java | Python | Go

StringJoinerクラス

より高度な結合が必要な場合は、StringJoiner クラスを使用します。

結合時の区切り文字だけでなく、接頭辞(Prefix)や接尾辞(Suffix)を指定できるのが特徴です。

Java
import java.util.StringJoiner;

public class StringJoinerExample {
    public static void main(String[] args) {
        // 区切り文字、接頭辞、接尾辞を指定
        StringJoiner sj = new StringJoiner(", ", "[", "]");
        
        sj.add("Red");
        sj.add("Green");
        sj.add("Blue");

        System.out.println("カラー設定: " + sj.toString());
    }
}
実行結果
カラー設定: [Red, Green, Blue]

ストリームAPIを用いた結合:Collectors.joining

Java 8のStream APIを使用している場合、コレクション内の要素を加工しながら結合したいケースがあります。

その際は Collectors.joining を利用するのがモダンな書き方です。

Java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamJoinExample {
    public static void main(String[] args) {
        List<String> users = Arrays.asList("alice", "bob", "charlie");

        // 全て大文字に変換してからカンマ区切りで結合
        String result = users.stream()
                             .map(String::toUpperCase)
                             .collect(Collectors.joining(", "));

        System.out.println("変換後ユーザー: " + result);
    }
}
実行結果
変換後ユーザー: ALICE, BOB, CHARLIE

この手法の強みは、フィルタリングやマッピングなどのストリーム処理の流れの中でシームレスに結合処理へ移行できる点にあります。

Java 15以降のテキストブロックによる結合

SQLクエリやJSON、HTMLなどの複数行にわたる文字列を扱う場合、これまではプラス演算子で各行を繋いでいました。

しかし、Java 15から導入された「テキストブロック」を使用することで、結合の手間を省きつつ可読性を劇的に向上させることができます。

Java
public class TextBlockExample {
    public static void main(String[] args) {
        // テキストブロックによる複数行文字列
        String json = """
                {
                    "name": "Java",
                    "version": 21,
                    "status": "LTS"
                }
                """;

        System.out.println(json);
    }
}
実行結果
{
    "name": "Java",
    "version": 21,
    "status": "LTS"
}

テキストブロック内部で変数を埋め込みたい場合は、後述する formatted() メソッドと組み合わせるのが一般的です。

フォーマットを指定した結合:String.formatとformatted

特定の形式(日付、数値の桁数指定、パディングなど)に合わせて文字列を組み立てるには、フォーマット指定子を使用する方法が最適です。

String.formatメソッド

古くからある手法ですが、テンプレート形式で記述できるため、結合箇所が多い場合にコードがスッキリします。

Java
public class FormatExample {
    public static void main(String[] args) {
        String item = "PC";
        int price = 150000;
        
        // %s は文字列、%,d は桁区切りの整数
        String message = String.format("商品名: %s, 価格: %,d円", item, price);
        
        System.out.println(message);
    }
}
実行結果
商品名: PC, 価格: 150,000円

formattedメソッド(Java 15以降)

Java 15からは、Stringインスタンスに対して直接呼び出せる formatted メソッドが追加されました。

テキストブロックとの相性が抜群です。

Java
public class FormattedMethodExample {
    public static void main(String[] args) {
        String name = "管理者";
        String message = "ログインに成功しました。";

        String template = """
                ユーザー: %s
                通知内容: %s
                """.formatted(name, message);

        System.out.println(template);
    }
}
実行結果
ユーザー: 管理者
通知内容: ログインに成功しました。

各手法のパフォーマンス比較と選び方

どの手法を選ぶべきか、主要な観点(可読性・パフォーマンス・用途)から比較表にまとめました。

手法パフォーマンス可読性推奨される用途
+ 演算子高(最適化時)最高数個程度の短い結合、デバッグログ
StringBuilder最高ループ内での大量結合
String.joinリストや配列を特定の文字で繋ぐ場合
Collectors.joining中〜高Stream API処理中での結合
formatted()複雑なテンプレートや桁数指定が必要な場合
Text Blocks最高SQLやJSONなど複数行にわたる定数

迷った時の判断基準

基本は「+」演算子

コンパイラが賢いため、通常の代入文での結合なら十分に高速です。

ループの中なら「StringBuilder」

ループの中ではStringBuilder一択です。

たとえ数回のループでも、習慣づけておくことで将来のボトルネックを防げます。

CSV形式など「区切り」があるなら「String.join」

最後の要素に余計なカンマがつくといった「末尾処理」のバグを防げます。

複数行なら「テキストブロック」

\n を手動で入れる手間と読みづらさから解放されます。

実践的な使い分けシナリオ

より具体的な開発シーンを想定して、どの手法を適用すべきか見ていきましょう。

シナリオ1:動的なSQLクエリの構築

検索条件に応じて WHERE 句を動的に組み立てる場合、条件の数によって結合回数が変わります。

このようなケースでは StringBuilder が最適です。

Java
public class SqlBuilder {
    public String buildQuery(String name, Integer age) {
        StringBuilder sql = new StringBuilder("SELECT * FROM users WHERE 1=1");
        
        if (name != null) {
            sql.append(" AND name = '").append(name).append("'");
        }
        if (age != null) {
            sql.append(" AND age = ").append(age);
        }
        
        return sql.toString();
    }
}

シナリオ2:ログメッセージの整形

例外が発生した際、エラーコードや詳細メッセージ、発生時刻などを整形して出力する場合は、formatted() が読みやすいでしょう。

Java
public class LogExample {
    public void logError(String code, String detail) {
        String logMsg = "[ERROR] Code:%s | Detail:%s | Time:%d"
                        .formatted(code, detail, System.currentTimeMillis());
        System.err.println(logMsg);
    }
}

注意点:String.concat() について

Javaには String.concat() というメソッドも存在しますが、現代のJava開発ではあまり使われません。

Java
String s = "Hello".concat(" World");

このメソッドは、引数が空文字の場合に新しいオブジェクトを作らないといった利点がありますが、null を渡すと NullPointerException が発生します。

また、複数の結合を行う際の可読性もプラス演算子に劣ります。

基本的にはプラス演算子や StringBuilder を優先して問題ありません。

まとめ

Javaの文字列結合は、言語の進化とともに多様な選択肢が提供されるようになりました。

単純な結合であれば「+」演算子の直感的な記述を活かし、パフォーマンスが求められるループ処理ではStringBuilderを徹底する、といったメリハリが重要です。

また、Java 8以降の String.join や、Java 15以降のテキストブロックformatted メソッドを使いこなすことで、コードの可読性は飛躍的に向上し、バグの入り込みにくいクリーンなソースコードを実現できます。

「動けばいい」という結合から、「意図と性能を両立させた」結合へ。

本記事で紹介した手法を、ぜひ日々のコーディングで活用してみてください。