如何实现 System.out.println(“a”) 显示 b
今天看到一篇文章不用反射,能否交换两个字符串的值. 心想字符串常量在常量池里面,是在就算用了反射也交换不了吧。转念一想,不对,字符串常量虽然本身在常量池里面,但是它依然是个对象,那么 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
是可以修改的,不仅可以指向常量池,也可以指向堆。