[{"data":1,"prerenderedAt":113},["ShallowReactive",2],{"content:\u002Ffiddling\u002Fthis-in-javac-string-concat":3,"surround:\u002Ffiddling\u002Fthis-in-javac-string-concat":102},{"id":4,"title":5,"body":6,"categories":80,"date":82,"description":83,"draft":84,"extension":85,"image":86,"meta":87,"navigation":89,"path":90,"permalink":86,"published":86,"readingTime":91,"recommend":86,"references":86,"seo":96,"sitemap":97,"stem":98,"tags":99,"type":100,"__hash__":101},"content\u002Fposts\u002Ffiddling\u002Fthis-in-javac-string-concat.md","Java 中 this 关键字导致编译期常量传播优化失效的问题",{"type":7,"value":8,"toc":77},"minimark",[9,13,24,27,35,38,44,47,53,68,71,74],[10,11,12],"p",{},"名字起的有点长了，但是这确实是个挺有趣的问题。如下代码：",[14,15,21],"pre",{"className":16,"code":18,"language":19,"meta":20},[17],"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","",[22,23,18],"code",{"__ignoreMap":20},[10,25,26],{},"我们先来猜一猜输出结果，第 7 行的 s 和第 8 行的 this.s 都是指的同一个变量，就是那个 final 静态变量 s。那么就算我们不知道结果，这两个输出应该也是一样的。但实际上的输出是",[14,28,33],{"className":29,"code":31,"language":32,"meta":20},[30],"language-shell","true\nfalse\n","shell",[22,34,31],{"__ignoreMap":20},[10,36,37],{},"我们先用 javap 将生成的字节码反汇编出来，test() 部分如下：",[14,39,42],{"className":40,"code":41,"language":32,"meta":20},[30],"  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",[22,43,41],{"__ignoreMap":20},[10,45,46],{},"常量表中的 #7 为：",[14,48,51],{"className":49,"code":50,"language":32,"meta":20},[30],"   #7 = String             #8             \u002F\u002F ab\n   #8 = Utf8               ab\n",[22,52,50],{"__ignoreMap":20},[10,54,55,56,59,60,63,64,67],{},"我们先来看第一个 true，这个大家应该基本都知道原因。编译器在将源码编译为 class 字节码文件时，会将当前类的方法中出现的 final 常量替换为字面量，于是 Java 代码第 6 行的 ",[22,57,58],{"code":58},"String ab1 = s + \"b\"","; 就变为 ",[22,61,62],{"code":62},"String ab1 = \"a\" + \"b\"",";，进一步，由于 ab1 是由两个字面量直接拼接的，编译器就直接帮其完成拼接，最终的结果，这条语句等价于 ",[22,65,66],{"code":66},"String ab1 = \"ab\";","。于是 cmp 和 ab1 都指向常量池的\"ab\"字符串，所以 cmp == ab1。反汇编的字节码中，第 0 行和第 3 行是完全一样的，ldc（Load Constant）的参数都是 #7。",[10,69,70],{},"字节码的 8~15 行是准备字符串 ab2 的过程，可以看到这里执行了一个动态方法调用，调用的是方法 makeConcatWithConstants，这个方法是 Java 的一个引导方法，用于处理 Java 中对 String 进行“+”拼接。这个方法会在堆中创建一个新的字符串变量，这就是 ab2 != cmp 的原因。",[10,72,73],{},"BTW，makeConcatWithConstants 这个方法在 JDK 9 中被引入用于处理字符串“+”操作，在 JDK 8 之前，javac 一直是用 StringBuilder 类来处理的。",[10,75,76],{},"那么是什么导致了这个差异呢？显然，问题在这个 this 关键字上。Java 在编译时，会隐式在所有成员方法中添加一个指向当前实例的引用 this，这个 this 在字节码中是作为方法参数传递给方法的，这就是为什么 void() 方法没有参数，字节码第五行中 args_size 却等于 1。对于一个对象引用的变量（无论是类变量还是成员变量），Java 编译器仅仅是很粗暴地关闭了这个优化。如果将这里的 this.s 改为 Test.s，那么这里输出的结果就是 true 了。",{"title":20,"searchDepth":78,"depth":78,"links":79},4,[],[81],"折腾","2022-04-16 00:01:28","在 Java 中，使用 `this` 关键字时，编译器对常量的优化可能会失效。代码示例中，尽管 `ab1` 和 `ab2` 看似引用了相同的 `final` 静态变量 `s`，但在比较时却产生了不同的结果。`ab1` 通过直接引用静态变量进行字符串连接，而 `ab2` 则是通过 `this` 关键字，导致编译器无法进行相同的常量传播优化，从而影响了字符串的比较结果。这一现象揭示了在 Java 中细微的语法差异可能引发的编译行为变化。",false,"md",null,{"slots":88},{},true,"\u002Ffiddling\u002Fthis-in-javac-string-concat",{"text":92,"minutes":93,"time":94,"words":95},"4 min read",3.735,224100,747,{"title":5,"description":83},{"loc":90},"posts\u002Ffiddling\u002Fthis-in-javac-string-concat",[81,19],"tech","59DffSOUqjQdxBIj2nXr5a6xpkQyvj143H0G0uDb98E",[103,108],{"title":104,"path":105,"stem":106,"date":107,"type":100,"children":-1},"CSAPP LAB 环境搭建","\u002Ffiddling\u002Fcsapplab0","posts\u002Ffiddling\u002Fcsapplab0","2021-12-27 00:09:00",{"title":109,"path":110,"stem":111,"date":112,"type":100,"children":-1},"golang 支持不同类型结构体间的深拷贝","\u002Ffiddling\u002Fgolang-deepcopy-between-different-type","posts\u002Ffiddling\u002Fgolang-deepcopy-between-different-type","2022-08-15 01:05:01",1787554445277]