今天看到一篇文章不用反射,能否交換兩個字串的值. 心想字串常數在常數池裡面,是在就算用了反射也交換不了吧。轉念一想,不對,字串常數雖然本身在常數池裡面,但是它依然是個物件,那麼 private final 型別的屬性僅僅表示它是一個指向常數池的參照,而並非不可修改。完全可以讓它指向另一個常數。
通過反射可以很輕鬆地獲取所有屬性
// 獲取所有屬性
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
是可以修改的,不僅可以指向常數池,也可以指向堆。