断り書き
- ある程度の知識があれば、知っているようなことしか書いていません
- 入門者・初心者に説明するときの資料のつもりです
目的
文字列での文字列連結についての、基礎の基礎を整理します。
Java でコーディングをする際、文字列連結について String
クラス、あるいは文字列リテラルを +
演算子や +=
代入演算子での結合ではなく StringBuilder
クラスの append()
メソッドを使用するようにという指摘を受けることがあると思います。
これは、実行時に無駄なインスタンスが生成されて、性能が劣化することを避けるための対策です。
しかし中には修正しなくても良い書き方を手間を掛けて書き直したり、逆に性能が悪くなる書き方に書き換えているケースも見受けられます。
ここでは、Javaの文字列連結時のケースについて、どちらの書き方が良いかを整理します。
結論
簡単に結論だけ書いておきます。
- 文字列リテラル同士の結合は、コンパイル時に行われる
- リテラルでない文字列オブジェクトと別のオブジェクトの連結は、一度
StringBuilder
の結合の形に展開された後に、最終的にString
に変換される - 無駄なオブジェクトの生成は少なくすること
- 性能も大切ですが、まずは分かりやすいこと・メンテしやすいことを意識して
ケーススタディ
ケース1:文字列リテラル同士の結合
文字列リテラル同士の結合は、コンパイル時に処理されます。
(文字列リテラルとは、直接文字列をダブルクオーテーションでくくってある表現のことです)
文字列定数(final な文字列変数で、文字列リテラルで初期化してあるもの。後述の例の TABLE_NAME
のようなものです)についても、文字列リテラルと同じようにコンパイル時に処理されます。
※例にはありませんが、プリミティブ型のリテラル、あるいは定数についても、コンパイル時に処理されます。
そのため StringBuilder#append()
で結合するよりも +
で結合するほうが性能的に良いです
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)のコンパイル結果を、逆コンパイルしたものです
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()
で連結すると、実行時に結合が行われます。
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
の結合でも、性能的にはほぼ変わりません。
読みやすく、分かりやすいと思う方を選べばよいと思います。
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)のコンパイル結果を、逆コンパイルしたものです
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
で連結した場合は、以下のようになります。
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
を +
で結合すると、何度も StringBuilder
や String
のインスタンスを生成することになるため、性能面で悪く、またリソース面でも問題があります(ガベージ・コレクションの回数が増えるなど)。
そのため StringBuilder#append()
で結合するほうが良いです。
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)のコンパイル結果を、逆コンパイルしたものです
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()
で連結した場合は、以下のようになります。
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() で十分な内容ですが、例示のためにループで実装しています)
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回実行されます。
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回実行されます。
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
は使わないで問題ないと思います。
もしマルチスレッド対応が必要であれば、別の方法で対策すればよいと思います(別のロックオブジェクトを持つなど)。