如何實現 System.out.println("a") 顯示 b

2022-08-12 18:04:40

今天看到一篇文章不用反射,能否交換兩個字串的值. 心想字串常數在常數池裡面,是在就算用了反射也交換不了吧。轉念一想,不對,字串常數雖然本身在常數池裡面,但是它依然是個物件,那麼 private final 型別的屬性僅僅表示它是一個指向常數池的參照,而並非不可修改。完全可以讓它指向另一個常數。

分析String的結構

通過反射可以很輕鬆地獲取所有屬性

// 獲取所有屬性
for (Field field : String.class.getDeclaredFields()) {
	System.out.println(field);
}

方框框起來的 private final byte[] java.lang.String.value 即為需要的物件。

設定可見性

接下來就是常見的反射修改可見性。

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

然而這一步會報錯:java.base does not 「opens java.lang「 to unnamed module,即非法存取警告。

這是因為 JDK 9 開始,除非模組標識為opens去允許反射存取,否則模組不能使用反射去存取非公有的成員/成員方法以及構造方法。解決方案為,設定VM啟動引數 --add-opens=java.base/java.lang.invoke=ALL-UNNAMED

參照 非法存取異常 以及 IDEA設定VMoptions

編寫顯示函數

希望顯示比較充分的資訊,但這樣反覆調格式就太麻煩了,所以封裝到函數裡。由於是採用的 main 入口函數,所以需要寫成靜態方法。

    private static void show(String s, String name, Field field) {
        StringBuilder sb = new StringBuilder();
        try {
            sb.append("String ").append(name).append("@").append(s.hashCode()).append("{")
                    .append("value@").append(Integer.toHexString(field.get(s).hashCode())).append(" = ").append(s)
                    .append("}");
            System.out.println(sb.toString());
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

編寫主函數

    public static void main(String[] args) {
        String a = "a";
        String b = "b";
        String c = "a";
        // 獲取所有屬性
        for (Field field : String.class.getDeclaredFields()) {
            System.out.println(field);
        }
        try {
            Field field = String.class.getDeclaredField("value");
            field.setAccessible(true);
            show(a, "a", field);
            show(b, "b", field);
            show(c, "c", field);
            field.set(a, field.get(b));
            show(a, "a", field);
            show(b, "b", field);
            show(c, "c", field);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

執行效果

String a@97{value@568db2f2 = a}
String b@98{value@378bf509 = b}
String c@97{value@568db2f2 = a}
String b@97{value@378bf509 = b}
String b@98{value@378bf509 = b}
String c@97{value@378bf509 = b}

其中前三行是執行前,後三行是執行後。

值得注意的是,第四行原本是希望顯示為:

String a@97{value@378bf509 = b}

而實際結果為:

這說明我們成功地修改了常數池中字串"a"的值,使其值為private final byte[] value = {'b'}

這也就有了題目,在main函數的最後補充以下程式碼:

System.out.println("\"a\"現在的值為:");
System.out.println("a");
field.set(a, new byte[] {65, 66, 67});
System.out.println("\"a\"現在的值為:");
System.out.println("a");

結果為:

可見 private final byte[] value 是可以修改的,不僅可以指向常數池,也可以指向堆。