Edited at

[Java] Stringの結合について

More than 1 year has passed since last update.


断り書き


  • ある程度の知識があれば、知っているようなことしか書いていません

  • 入門者・初心者に説明するときの資料のつもりです


目的

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

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ステートメントで結合する場合には、どちらもほぼ同じ


ケース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() で結合するほうが良い


おまけ


文字の結合

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 は使わないで問題ないと思います。

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