[{"data":1,"prerenderedAt":121},["ShallowReactive",2],{"content:\u002Fja\u002Ffiddling\u002Fthis-in-javac-string-concat":3,"surround:\u002Fja\u002Ffiddling\u002Fthis-in-javac-string-concat":110},{"id":4,"title":5,"authorship":6,"body":7,"categories":81,"date":83,"description":84,"draft":85,"extension":86,"image":87,"meta":88,"navigation":90,"path":91,"permalink":92,"published":92,"readingTime":93,"recommend":92,"references":92,"seo":98,"seoDescription":99,"seoTitle":92,"sitemap":100,"stem":101,"tags":102,"type":108,"__hash__":109},"content_ja\u002Fposts\u002Ffiddling\u002Fthis-in-javac-string-concat.md","Javaのthisキーワードでコンパイル時の定数伝播が効かなくなる問題","human-only",{"type":8,"value":9,"toc":78},"minimark",[10,14,25,28,36,39,45,48,54,69,72,75],[11,12,13],"p",{},"少し長いタイトルになりましたが、なかなか面白い問題です。次のコードを見てください。",[15,16,22],"pre",{"className":17,"code":19,"language":20,"meta":21},[18],"language-java","public class Test {\n    final static String s = \"a\";\n\n    public void test() {\n        String cmp = \"ab\";\n        String ab1 = s + \"b\";\n        String ab2 = this.s + \"b\";\n        System.out.println(ab1 == cmp);\n        System.out.println(ab2 == cmp);\n    }\n\n    public static void main(String[] args) {\n        new Test().test();\n    }\n}\n","java","",[23,24,19],"code",{"__ignoreMap":21},[11,26,27],{},"まず出力を予想してみましょう。7行目のsと8行目のthis.sは、同じstatic final変数sを指しています。結果が分からなくても、少なくとも2つの出力は同じになりそうです。ところが実際にはこうなります。",[15,29,34],{"className":30,"code":32,"language":33,"meta":21},[31],"language-shell","true\nfalse\n","shell",[23,35,32],{"__ignoreMap":21},[11,37,38],{},"javapで生成されたバイトコードを逆アセンブルしてみます。test()の部分は次のとおりです。",[15,40,43],{"className":41,"code":42,"language":33,"meta":21},[31],"  public void test();\n    descriptor: ()V\n    flags: (0x0001) ACC_PUBLIC\n    Code:\n      stack=3, locals=4, args_size=1\n         0: ldc           #7                  \u002F\u002F String ab\n         2: astore_1\n         3: ldc           #7                  \u002F\u002F String ab\n         5: astore_2\n         6: aload_0\n         7: pop\n         8: ldc           #11                 \u002F\u002F String a\n        10: invokedynamic #13,  0             \u002F\u002F InvokeDynamic #0:makeConcatWithConstants:(Ljava\u002Flang\u002FString;)Ljava\u002Flang\u002FString;\n        15: astore_3\n        16: getstatic     #17                 \u002F\u002F Field java\u002Flang\u002FSystem.out:Ljava\u002Fio\u002FPrintStream;\n        19: aload_2\n        20: aload_1\n        21: if_acmpne     28\n        24: iconst_1\n        25: goto          29\n        28: iconst_0\n        29: invokevirtual #23                 \u002F\u002F Method java\u002Fio\u002FPrintStream.println:(Z)V\n        32: getstatic     #17                 \u002F\u002F Field java\u002Flang\u002FSystem.out:Ljava\u002Fio\u002FPrintStream;\n        35: aload_3\n        36: aload_1\n        37: if_acmpne     44\n        40: iconst_1\n        41: goto          45\n        44: iconst_0\n        45: invokevirtual #23                 \u002F\u002F Method java\u002Fio\u002FPrintStream.println:(Z)V\n        48: return\n",[23,44,42],{"__ignoreMap":21},[11,46,47],{},"定数プールの#7は次の内容です。",[15,49,52],{"className":50,"code":51,"language":33,"meta":21},[31],"   #7 = String             #8             \u002F\u002F ab\n   #8 = Utf8               ab\n",[23,53,51],{"__ignoreMap":21},[11,55,56,57,60,61,64,65,68],{},"まず最初のtrueは、よく知られた話だと思います。ソースをclassバイトコードにコンパイルするとき、コンパイラはそのクラスのメソッド内に現れるfinal定数をリテラルに置き換えます。そのためJavaコード6行目の",[23,58,59],{"code":59},"String ab1 = s + \"b\"",";は",[23,62,63],{"code":63},"String ab1 = \"a\" + \"b\"",";になります。さらに、リテラル同士の連結なのでコンパイラが連結も済ませ、最終的には",[23,66,67],{"code":67},"String ab1 = \"ab\";","と同等になります。cmpとab1はどちらも定数プールの文字列\"ab\"を指すため、cmp == ab1です。逆アセンブルしたバイトコードの0行目と3行目はまったく同じで、ldc（Load Constant）の引数はいずれも#7です。",[11,70,71],{},"バイトコードの8〜15行目は、文字列ab2を用意する処理です。ここではmakeConcatWithConstantsというメソッドが動的に呼び出されています。これはJavaでStringを「+」で連結するためのブートストラップメソッドです。このメソッドはヒープに新しい文字列を作るので、ab2 != cmpになります。",[11,73,74],{},"ちなみにmakeConcatWithConstantsは、文字列の「+」演算を処理するためJDK 9で導入されました。JDK 8より前のjavacは、ずっとStringBuilderを使っていました。",[11,76,77],{},"では、何がこの違いを生むのでしょうか。原因は明らかにthisキーワードです。Javaはコンパイル時に、すべてのインスタンスメソッドへ現在のインスタンスを指す参照thisを暗黙に追加します。バイトコードではthisをメソッドの引数として渡します。void()には引数がないのに、バイトコード5行目のargs_sizeが1なのはこのためです。クラス変数でもインスタンス変数でも、オブジェクト参照経由でアクセスする変数に対しては、Javaコンパイラはこの最適化を一律に無効にしてしまいます。ここをthis.sからTest.sに変えると、出力はtrueになります。",{"title":21,"searchDepth":79,"depth":79,"links":80},4,[],[82],"fiddling","2022-04-16 00:01:28","Javaでは`this`を使うことで、コンパイラの定数最適化が効かなくなる場合があります。このコード例の`ab1`と`ab2`は同じstatic final変数`s`を参照しているように見えますが、比較結果は異なります。静的変数を直接参照して文字列を連結する`ab1`に対し、`this`経由の`ab2`では同じ定数伝播の最適化が行われません。わずかな構文の違いが、コンパイル結果を変える例です。",false,"md","https:\u002F\u002Fblog-img.774352199.xyz\u002FgKtkYe.webp",{"slots":89},{},true,"\u002Ffiddling\u002Fthis-in-javac-string-concat",null,{"text":94,"minutes":95,"time":96,"words":97},"4 min read",3.88,232800,776,{"title":5,"description":84},"Javaの文字列結合でsとthis.sの比較結果が異なる理由を、javapのバイトコードで確認。定数伝播、定数プールの参照、実行時の文字列生成の違いを追います。",{"loc":91},"posts\u002Ffiddling\u002Fthis-in-javac-string-concat",[103,104,105,106,107],"Java","javac","文字列連結","定数伝播","バイトコード","tech","Iev5nPGgyIAIWSmKyK1LHDgap4nZpyMHn5BC--e5An0",[111,116],{"title":112,"path":113,"stem":114,"date":115,"type":108,"children":-1},"CSAPP Labの環境構築","\u002Ffiddling\u002Fcsapplab0","posts\u002Ffiddling\u002Fcsapplab0","2021-12-27 00:09:00",{"title":117,"path":118,"stem":119,"date":120,"type":108,"children":-1},"Goで異なる型の構造体をディープコピーする","\u002Ffiddling\u002Fgolang-deepcopy-between-different-type","posts\u002Ffiddling\u002Fgolang-deepcopy-between-different-type","2022-08-15 01:05:01",1789914058764]