透過 javap -c 反編譯字節碼,深入理解 String s1 = "a"; s1 + "b" 與 "a" + "b" 在記憶體與常量池的不同行為。
public class StringDemo {
public static void aa() {
String s1 = "a"; // 常量池
String s2 = "b"; // 常量池
String s12 = "ab"; // 常量池
String s3 = s1 + s2; // 執行期 new StringBuilder().append().toString() → 堆
System.out.println(s3 == s12); // false
}
public static void bb() {
String s1 = "a" + "b"; // 編譯期常量折疊 → "ab" 進常量池
String s2 = "ab"; // 常量池同一引用
System.out.println(s1 == s2); // true
}
}
0: ldc #3 // "a" → 常量池 2: astore_0 3: ldc #4 // "b" → 常量池 5: astore_1 6: ldc #5 // "ab" → 常量池 8: astore_2 9: new #6 // new StringBuilder 12: dup 13: invokespecial #7 // StringBuilder.16: aload_0 17: invokevirtual #8 // append("a") 20: aload_1 21: invokevirtual #8 // append("b") 24: invokevirtual #9 // toString() → new String(heap) 27: astore_3 // s3 指向堆記憶體 28: aload_3 29: aload_2 30: if_acmpne 37 // 比較引用地址:堆 vs 常量池 → 不同 33: iconst_1 34: goto 38 37: iconst_0 38: invokestatic #10 // Boolean.valueOf 41: invokestatic #11 // print(false) 44: return
ldc:將常量池字面值推入棧頂new / invokespecial:建立 StringBuilder 實例invokevirtual:呼叫實例方法 append、toStringtoString 內部 new String(value, 0, count) → 堆記憶體新物件if_acmpne:比較兩引用地址是否相同0: ldc #5 // "ab" → 常量池(編譯期常量折疊) 2: astore_0 3: ldc #5 // 同一常量池項目 5: astore_1 6: aload_0 7: aload_1 8: if_acmpne 15 // 引用相同 → true 11: iconst_1 12: goto 16 15: iconst_0 16: invokestatic #10 // Boolean.valueOf(true) 19: invokestatic #11 // print(true) 22: return
"a" + "b" 為編譯期常量表達式,javac 直接折疊為 "ab" 存入常量池,故 s1、s2 指向同一引用。
iconst_0/1:將 int 0/1 推入棧頂,boolean 在 JVM 中佔 4 bytes(同 int)Boolean.valueOf():基本型別自動裝箱為包裝類+ 非常量表達式時,編譯器生成 StringBuilder 代碼(JDK 1.5+)equals()StringBuilder 或 StringJoiner== 誤判