せてぃーずノート

Javaのイベント参加レポートとかを書いたりします。

文字列結合とバイトコード

今日もバイトコードのお勉強。
文字列の+演算子の結合パターンを色々と。

まずはリテラルオンリー

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に辿り着くまで道は長そう。