LoginSignup
104
87

More than 1 year has passed since last update.

[Java] Stringの結合について

Last updated at Posted at 2015-11-25

断り書き

  • ある程度の知識があれば、知っているようなことしか書いていません
  • 入門者・初心者に説明するときの資料のつもりです

目的

文字列での文字列連結についての、基礎の基礎を整理します。

Java でコーディングをする際、文字列連結について String クラス、あるいは文字列リテラルを + 演算子や += 代入演算子での結合ではなく StringBuilder クラスの append() メソッドを使用するようにという指摘を受けることがあると思います。

これは、実行時に無駄なインスタンスが生成されて、性能が劣化することを避けるための対策です。

しかし中には修正しなくても良い書き方を手間を掛けて書き直したり、逆に性能が悪くなる書き方に書き換えているケースも見受けられます。

ここでは、Javaの文字列連結時のケースについて、どちらの書き方が良いかを整理します。

結論

簡単に結論だけ書いておきます。

  • 文字列リテラル同士の結合は、コンパイル時に行われる
  • リテラルでない文字列オブジェクトと別のオブジェクトの連結は、一度 StringBuilder の結合の形に展開された後に、最終的に String に変換される
  • 無駄なオブジェクトの生成は少なくすること
  • 性能も大切ですが、まずは分かりやすいこと・メンテしやすいことを意識して

ケーススタディ

ケース1:文字列リテラル同士の結合

文字列リテラル同士の結合は、コンパイル時に処理されます。
(文字列リテラルとは、直接文字列をダブルクオーテーションでくくってある表現のことです)

文字列定数(final な文字列変数で、文字列リテラルで初期化してあるもの。後述の例の TABLE_NAME のようなものです)についても、文字列リテラルと同じようにコンパイル時に処理されます。

※例にはありませんが、プリミティブ型のリテラル、あるいは定数についても、コンパイル時に処理されます。

そのため StringBuilder#append() で結合するよりも + で結合するほうが性能的に良いです

Case1_1.java
    private static final String TABLE_NAME = "TABLE";

    private static String case1_1() {
        String data = "SELECT "
                + "ITEM1, ITEM2, ITEM3 "
                + "FROM " + TABLE_NAME
                + " WHERE "
                + " ITEM1 = ?";
        return data;
    }

上記 Case1_1.java のコンパイル結果を、逆コンパイルすると以下のようになります。

※Eclipseのコンパイラ(Eclipse Compiler for Java)のコンパイル結果を、逆コンパイルしたものです

Case1_1.java
    private static final String TABLE_NAME = "TABLE";

    private static String case1_1()
    {
        String data = "SELECT ITEM1, ITEM2, ITEM3 FROM TABLE WHERE  ITEM1 = ?";
        return data;
    }

一方、StringBuilder#append() で連結すると、実行時に結合が行われます。

Case1_2.java
    private static final String TABLE_NAME = "TABLE";

    private static String case1_2() {
        StringBuilder buff = new StringBuilder();
        buff.append("SELECT ");
        buff.append("ITEM1, ITEM2, ITEM3 ");
        buff.append("FROM ");
        buff.append(TABLE_NAME);
        buff.append(" WHERE ");
        buff.append(" ITEM1 = ?");
        return buff.toString();
    }

結論:文字列リテラル同士の結合は + で結合するほうが良い

ケース2:1ステートメントでの文字列結合

1ステートメントで文字列連結をする場合には、コンパイル時に StringBuilder#append() を使う形に展開されます。

結果的に String の連結でも StringBuilder の結合でも、性能的にはほぼ変わりません。

読みやすく、分かりやすいと思う方を選べばよいと思います。

Case2_1.java
    private static String case2_1(String name, String value) {
        String data = "name: " + name + ", "
                + "value: " + value;
        return data;
    }

上記 Case2_1.java のコンパイル結果を、逆コンパイルすると以下のようになります。

※Eclipseのコンパイラ(Eclipse Compiler for Java)のコンパイル結果を、逆コンパイルしたものです

Case2_1.java
    private static String case2_1(String name, String value)
    {
        String data = (new StringBuilder("name: ")).append(name).append(", ").append("value: ").append(value).toString();
        return data;
    }

StringBuilder で連結した場合は、以下のようになります。

Case2_2.java
    private static String case2_2(String name, String value) {
        StringBuilder buff = new StringBuilder();
        buff.append("name: ");
        buff.append(name);
        buff.append(", value: ");
        buff.append(value);
        return buff.toString();
    }

結合:1ステートメントで結合する場合には、どちらもほぼ同じ

(Java9 からは StringBuilder に展開されるのではなく、InvokeDynamicで処理されるようです。JEP 280: Indify String Concatenation

ケース3:複数ステートメントにわたっての文字列結合

複数ステートメントにわたって文字列連結をする場合には String+ で結合すると、何度も StringBuilderString のインスタンスを生成することになるため、性能面で悪く、またリソース面でも問題があります(ガベージ・コレクションの回数が増えるなど)。

そのため StringBuilder#append() で結合するほうが良いです。

Case3_1.java
    private static String case3_1(String name, String value) {
        String data = "name: " + name + "\r\n";
        data += "value: " + value + "\r\n";
        return data;
    }

この例では2回しかしていませんが、例えばループの中での結合も同様です(性能的にはさらに悪い)。

上記 Case3_1.java のコンパイル結果を、逆コンパイルすると以下のようになります。各行の最初で new StringBuilder() を、最後で StringBuilder#toString() を呼んでいることが分かります。

※Eclipseのコンパイラ(Eclipse Compiler for Java)のコンパイル結果を、逆コンパイルしたものです

Case3_1.java
    private static String case3_1(String name, String value)
    {
        String data = (new StringBuilder("name: ")).append(name).append("\r\n").toString();
        data = (new StringBuilder(String.valueOf(data))).append("value: ").append(value).append("\r\n").toString();
        return data;
    }

StringBuilder#append() で連結した場合は、以下のようになります。

Case3_2.java
    private static String case3_2(String name, String value) {
        StringBuilder buff = new StringBuilder();
        buff.append("name: ");
        buff.append(name);
        buff.append("\r\n");
        buff.append("value: ");
        buff.append(value);
        buff.append("\r\n");
        return buff.toString();
    }

結論:複数ステートメントにわたって文字列結合する場合には StringBuilder#append() で結合するほうが良い

ケース4:複数ステートメントにわたっての文字列結合

ケース3と同じですが、実際にループの中で文字列結合する場合の差を見てみます。

ループ内で + で結合する例は以下のようになると思います(内容的には join() で十分な内容ですが、例示のためにループで実装しています)

Case4_1.java
    private static String case4_1(List<String> values) {
        String data = "[";
        for (String value : values) {
            data += value;
        }
        data += "]";
        return data;
    }

逆コンパイルすると以下のようになると思います。
new StringBuilder() が要素数+1回、 StringBuilder#toString() が要素数+1回実行されます。

Case4_1.java
    private static String case4_1_after(List<String> values) {
        String data = "[";
        for (String value : values) {
            data = (new StringBuilder(data)).append(value).toString();
        }
        data = (new StringBuilder(data)).append("]").toString();
        return data;
    }

一方 StringBuilder#append() で連結した場合は、以下のようになります。
new StringBuilder() が1回、 StringBuilder#append() が要素数+1回、StringBuilder#toString() が1回実行されます。

Case4_2.java
    private static String case4_2(List<String> values) {
        StringBuilder data = new StringBuilder("[");
        for (String value : values) {
            data.append(value);
        }
        data.append("]");
        return data.toString();
    }

おまけ

文字の結合

StringBuilder#append() で結合する場合、結合対象が1文字の場合には文字列( String )ではなく文字( char ) で結合するほうが良いです。

    buff.append("\n"); // 1文字の文字列
    buff.append('\n'); // 文字(こっちのほうが速い)

※2文字以上の文字列の場合に、無理に append(char) に置き換える必要は無いと思います。

部分文字列の結合

文字列の一部のみを切り出して結合する場合には String#substring(int,int)StringBuilder#append(String) の組み合わせではなく StringBuilder#append(CharSequence,int,int) を使うほうが良いです。

    buff.append(data.substring(4, 6)); // substringで切り出して append
    buff.append(data, 4, 6); // 専用 append メソッド(こっちのほうが速い)

使用する容量が分かる場合には初期サイズを指定する

StringBuilder のオブジェクトを生成する際に、最終的に使用する容量が分かっている場合には、初期サイズを指定するほうが良いことが多いです。

ただし使用容量が分からない場合に、無駄に大きなサイズを指定することは避けるべきです。

また、無理して使用するサイズを調べてまで初期容量を指定する必要はないと思います。

StringBuffer は使わない

StringBuffer は各メソッドで同期がかかります。
しかし、同じ文字列( StringBuffer なり StringBuilder なり)を複数スレッドから更新することはまず無いと思います。

基本的に StringBuffer は使わないで問題ないと思います。

もしマルチスレッド対応が必要であれば、別の方法で対策すればよいと思います(別のロックオブジェクトを持つなど)。

104
87
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
104
87