文字列結合とバイトコード
今日もバイトコードのお勉強。
文字列の+演算子の結合パターンを色々と。
まずはリテラルオンリー
public static void main(String[] args) {
System.out.println("Hello " + " World" + "!!");
String s = "寿限無 寿限無 "
+ "五劫の摺り切れ"
+ "海砂利水魚の"
+ "水行末 雲来末 風来末"
+ "食う寝る所に住む所"
+ "藪柑子 ブラコウジ"
+ "パイポ パイポ パイポの シューリンガン"
+ "シューリンガンのグーリンダイ"
+ "グーリンダイのポンポコピーの"
+ "ポンポコナーの"
+ "長久命の長助";
System.out.println(s);
}
上のソースのように、リテラルの文字列を+で結合した場合はコンパイル時に結合してくれる。
だからSQLなどの長い文字列を書く場合は遠慮せずに結合しちゃっていい。
間違えてもStringBuilderとかはやっちゃダメ。
ちなみにバイトコードはこんな感じ。
0 getstatic java.lang.System.out : java.io.PrintStream [16]
3 ldc <String "Hello World!!"> [22] // ←結合済みの文字列
5 invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]
8 ldc <String "寿限無 寿限無 五劫の摺り切れ海砂利水魚の水行末 雲来末 風来末食う寝る所に住む所藪柑子 ブラコウジパイポ パイポ パイポの シューリンガンシューリンガンのグーリンダイグーリンダイのポンポコピーのポンポコナーの長久命の長助"> [30] // ←長くても結合してくれる
10 astore_1 [s]
11 getstatic java.lang.System.out : java.io.PrintStream [16]
14 aload_1 [s]
15 invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]
18 return
Stringだけではなく、intなどの数値が入っていた場合でも大丈夫。
public static void main(String[] args) {
System.out.println(2014 + "Year!" + "Hello World" + "!!");
System.out.println("Hello " + 2014.1 + " World" + "!!");
System.out.println("Hello " + " World" + 11);
}
上のようなソースでも、バイトコードでは結合した文字列になる。
intの部分がどこで出てきても大丈夫。
0 getstatic java.lang.System.out : java.io.PrintStream [22]
3 ldc <String "2014Year!Hello World!!"> [28]
5 invokevirtual java.io.PrintStream.println(java.lang.String) : void [30]
8 getstatic java.lang.System.out : java.io.PrintStream [22]
11 ldc <String "Hello 2014.1 World!!"> [36]
13 invokevirtual java.io.PrintStream.println(java.lang.String) : void [30]
16 getstatic java.lang.System.out : java.io.PrintStream [22]
19 ldc <String "Hello World11"> [38]
21 invokevirtual java.io.PrintStream.println(java.lang.String) : void [30]
24 return
足し算が発生する場合でも大丈夫。
public static void main(String[] args) {
System.out.println(20 + 14 + "Year!" + "Hello World" + "!!");
System.out.println("Hello World" + (20 + 14) + "!!");
}
バイトコードになった時は計算した後の値を使用したリテラルになっている。
0 getstatic java.lang.System.out : java.io.PrintStream [22]
3 ldc <String "34Year!Hello World!!"> [28] // 計算されてる!
5 invokevirtual java.io.PrintStream.println(java.lang.String) : void [30]
8 getstatic java.lang.System.out : java.io.PrintStream [22]
11 ldc <String "Hello World34!!"> [36] // 計算されてる!
13 invokevirtual java.io.PrintStream.println(java.lang.String) : void [30]
16 return
例えば、int mega = 1024 * 1024;という式はコンパイル時に計算した結果が代入される。
変数を含む場合
以下のように文字列結合に変数を含む場合は状況が異なる。
public static void main(String[] args) {
int i = 2014;
System.out.println(i + "Year!" + "Hello World" + "!!");
}
ソースを読めば、iが2014以外になることはありえない。
でもコンパイルしたバイトコードでは、StringBuilderを利用して文字列を結合している。
0 sipush 2014
3 istore_1 [i]
4 getstatic java.lang.System.out : java.io.PrintStream [22]
7 new java.lang.StringBuilder [28]
10 dup
11 iload_1 [i]
12 invokestatic java.lang.String.valueOf(int) : java.lang.String [30]
15 invokespecial java.lang.StringBuilder(java.lang.String) [36]
18 ldc <String "Year!"> [39]
20 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [41]
23 ldc <String "Hello World"> [45]
25 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [41]
28 ldc <String "!!"> [47]
30 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [41]
33 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [49]
36 invokevirtual java.io.PrintStream.println(java.lang.String) : void [53]
39 return
バイトコードの処理の通りにソースコードを直すと以下のようになる。
public static void main(String[] args) {
int i = 2013;
System.out.println(
new StringBuilder(String.valueOf(i))
.append("Year!")
.append("Hello World")
.append("!!").toString());
}
「文字列結合すると中間的なStringオブジェクトが生成されて効率が云々」みたいなことを言う人がいる。
しかし、コンパイルされたバイトコードを見るとStringBuilderを使って効率的な文字列結合が行われている。
ループが絡むと話が違ってくるけど、コンパイラがいい感じに処理してくれるので何が何でもStringBuilderを使う必要はない。
変数の出現位置によっては、事前に結合してくれる。
public static void main(String[] args) throws Exception {
System.out.println("SELECT" + " * " + "FROM" +
" PIYO " + "WHERE" + " ID " + "=" + args[0] +
" ORDER" + " BY " + "NAME");
}
上の例の場合、args[0]までの文字列は結合しているが、それ以降の文字列は1つずつappendしている。
0 getstatic java.lang.System.out : java.io.PrintStream [29]
3 new java.lang.StringBuilder [35]
6 dup
7 ldc <String "SELECT * FROM PIYO WHERE ID ="> [37]
9 invokespecial java.lang.StringBuilder(java.lang.String) [39]
12 aload_0 [args]
13 iconst_0
14 aaload
15 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [42]
18 ldc <String " ORDER"> [46]
20 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [42]
23 ldc <String " BY "> [48]
25 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [42]
28 ldc <String "NAME"> [50]
30 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [42]
33 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [52]
36 invokevirtual java.io.PrintStream.println(java.lang.String) : void [56]
39 return
定数を使った場合
変数が絡む場合でも、staticかつfinalで宣言された『定数』の場合はリテラルと同じ扱いになる。
private static final int YEAR = 2014;
public static void main(String[] args) {
System.out.println(YEAR + "Year!" + "Hello World" + "!!");
}
変数の場合と異なりコンパイル時に文字列を結合している。
0 getstatic java.lang.System.out : java.io.PrintStream [26]
3 ldc <String "2014Year!Hello World!!"> [32]
5 invokevirtual java.io.PrintStream.println(java.lang.String) : void [34]
8 return
ローカル変数をfinalで宣言した場合でも結果は同じになる。
効率を追求するならローカル変数すべてをfinalにするといいかもしれない。(大人しく定数にしろと言われそうだけど・・・)
今日のまとめ
コンパイラが割りといい感じに処理してくれるので、文字列結合は全部StringBuilderにするという置換は不要。
やってもやらなくてもあまり変わりないので、ソースが読みやすい方を選択した方がいい。
static finalを付けない定数もどきは意味が無い。
今日は思わぬ寄り道。
Lambdaに辿り着くまで道は長そう。