如何实现 System.out.println(“a”) 显示 b

今天看到一篇文章不用反射,能否交换两个字符串的值. 心想字符串常量在常量池里面,是在就算用了反射也交换不了吧。转念一想,不对,字符串常量虽然本身在常量池里面,但是它依然是个对象,那么 private final 类型的属性仅仅表示它是一个指向常量池的引用,而并非不可修改。完全可以让它指向另一个常量。

分析String的结构

通过反射可以很轻松地获取所有属性

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

image

方框框起来的 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}

而实际结果为:

image

这说明我们成功地修改了常量池中字符串"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");

结果为:

image

可见 private final byte[] value 是可以修改的,不仅可以指向常量池,也可以指向堆。

Tags: