Java 反射基础(下)

投稿作者:芮成兵/csdn 原文链接: http://blog.csdn.net/My_TrueLove/article/details/51306921

在上一篇博客《 Java 反射基础(上)》中,记录了如何在运行时获取类的所有变量和方法,还没看的读者可以看一下。

都知道,对象是无法访问或操作类的私有变量和方法的,但是,通过反射,我们就可以做到!没错,反射可以做到!今天,将在上一次记录的基础上继续探讨如何利用反射访问 类对象的私有方法 以及修改 私有变量或常量,绝对干货,我都喝了好几杯水了!话不多说,太渴了,这就开始。

准备测试类

老规矩,先上测试类。注:请注意看测试类中变量和方法的修饰符(访问权限);另外,测试类仅供测试,不提倡实际开发时这么写 : )

public class TestClass {

    private String MSG = "Original";

    private void privateMethod(String head , int tail){
        System.out.print(head + tail);
    }

    public String getMsg(){
        return MSG;
    }
}

访问私有方法

以访问 TestClass 类中的私有方法 privateMethod(...) 为例,方法加参数是为了考虑最全的情况,很贴心有木有?先贴代码,看注释,最后我会重点解释部分代码。

/**
 * 访问对象的私有方法
 * 为简洁代码,在方法上抛出总的异常,实际开发别这样
 */
private static void getPrivateMethod() throws Exception{
    //1. 获取 Class 类实例
    TestClass testClass = new TestClass();
    Class mClass = testClass.getClass();

    //2. 获取私有方法
    //第一个参数为要获取的私有方法的名称
    //第二个为要获取方法的参数的类型,参数为 Class...,没有参数就是null
    //方法参数也可这么写 :new Class[]{String.class , int.class}
    Method privateMethod =
            mClass.getDeclaredMethod("printMsg", String.class, int.class);

    //3. 开始操作方法
    if (privateMethod != null) {
        //获取私有方法的访问权
        //只是获取访问权,并不是修改实际权限
        privateMethod.setAccessible(true);

        //使用 invoke 反射调用私有方法
        //privateMethod 是获取到的私有方法
        //testClass 要操作的对象
        //后面两个参数传实参
        privateMethod.invoke(testClass, "Java Reflect ", 666);
    }
}

需要注意的是,第3步中的 setAccessible(true) 方法,是获取私有方法的访问权限,如果不加会报非法访问异常,因为当前方法访问权限是“private”的:

java.lang.IllegalAccessException: 
Class MainClass can not access a member of
class obj.TestClass with modifiers "private"

正常运行后,打印如下,调用私有方法成功:

Java Reflect 666

修改私有变量

以修改 TestClass 类中的私有变量 MSG 为例,其初始值为 “Original” ,我们要修改为 “Modified”。老规矩,先上代码看注释。

/**
 * 修改对象私有变量的值
 * 为简洁代码,在方法上抛出总的异常,实际开发别这样
 */
private static void modifyPrivateFiled() throws Exception {
    //1. 获取 Class 类实例
    TestClass testClass = new TestClass();
    Class mClass = testClass.getClass();

    //2. 获取私有变量
    Field privateField = mClass.getDeclaredField("MSG");

    //3. 操作私有变量
    if (privateField != null) {
        //获取私有变量的访问权
        privateField.setAccessible(true);

        //修改私有变量,并输出以测试
        System.out.println("Before Modify:MSG = " + testClass.getMsg());

        //调用 set(object , value) 修改变量的值
        //privateField 是获取到的私有变量
        //testClass 要操作的对象
        //"Modified" 为要修改成的值
        privateField.set(testClass, "Modified");
        System.out.println("After Modify:MSG = " + testClass.getMsg());
    }
}

此处代码和访问私有方法的逻辑差不多,就不再赘述,看打印结果,修改私有变量 成功:

Before Modify:MSG = Original
After Modify:MSG = Modified

修改私有常量

真的能修改吗?

私有常量是指使用 private final 修饰符修饰的常量,在上面介绍了如何修改私有变量,现在来说说如何修改私有常量,区别就在于有无 final 关键字修饰。在说之前,先补充一个知识点。

Java 虚拟机(JVM)在编译 .java 文件得到 .class 文件时,会优化我们的代码以提升效率。其中一个优化就是:JVM 在编译阶段会把引用常量的代码替换成具体的常量值,如下所示(部分代码)。

编译前的 .java 文件:

//注意是 String  类型的值
private final String FINAL_VALUE = "hello";

if(FINAL_VALUE.equals("world")){
    //do something
}

编译后得到的 .class 文件(当然,编译后是没有注释的):

private final String FINAL_VALUE = "hello";
//替换为"hello"
if("hello".equals("world")){
    //do something
}

但是,并不是所有常量都会优化。经测试对于 intlongboolean 以及 String 这些基本类型 JVM 会优化,而对于 IntegerLongBoolean 这种包装类型,或者其他诸如 DateObject 类型则不会被优化。

总结来说:对于基本类型的静态常量,JVM 在编译阶段会把引用此常量的代码替换成具体的常量值

这么说来,在实际开发中,如果我们想修改某个类的常量值,恰好那个常量是基本类型的,岂不是无能为力了?反正我个人认为除非修改源码,否则真没办法!

无能为力是指:我们在程序运行时刻依然可以使用反射修改常量的值(后面会代码验证),但是 JVM 在编译阶段得到的 .class 文件已经将常量优化为具体的值,在运行阶段就直接使用具体的值了,所以即使修改了常量的值也已经毫无意义了,No Sense

下面我们验证这一点,在测试类 TestClass 类中添加如下代码:

//String 会被 JVM 优化
private final String FINAL_VALUE = "FINAL";

public String getFinalValue(){
    //会被优化为: return "FINAL" ,拭目以待吧
    return FINAL_VALUE;
}

接下来,是修改常量的值,先上代码,请仔细看注释:

/**
 * 修改对象私有常量的值
 * 为简洁代码,在方法上抛出总的异常,实际开发别这样
 */
private static void modifyFinalFiled() throws Exception {
    //1. 获取 Class 类实例
    TestClass testClass = new TestClass();
    Class mClass = testClass.getClass();

    //2. 获取私有常量
    Field finalField = mClass.getDeclaredField("FINAL_VALUE");

    //3. 修改常量的值
    if (finalField != null) {

        //获取私有常量的访问权
        finalField.setAccessible(true);

        //调用 finalField 的 getter 方法
        //输出 FINAL_VALUE 修改前的值
        System.out.println("Before Modify:FINAL_VALUE = "
                + finalField.get(testClass));

        //修改私有常量
        finalField.set(testClass, "Modified");

        //调用 finalField 的 getter 方法
        //输出 FINAL_VALUE 修改后的值
        System.out.println("After Modify:FINAL_VALUE = "
                + finalField.get(testClass));

        //使用对象调用类的 getter 方法
        //获取值并输出
        System.out.println("Actually :FINAL_VALUE = "
                + testClass.getFinalValue());
    }
}

上面的代码不解释了,注释巨详细有木有!特别注意一下第3步的注释,然后来看看输出,已经迫不及待了,擦亮双眼:

Before Modify:FINAL_VALUE = FINAL
After Modify:FINAL_VALUE = Modified
Actually :FINAL_VALUE = FINAL

结果出来了:

第一句打印修改前 FINAL_VALUE 的值,没有异议;

第二句打印修改后变量的值,说明FINAL_VALUE确实通过反射修改了;

第三局打印通过 getFinalValue() 方法获取的 FINAL_VALUE 的值,但还是初始值,导致修改无效!

这结果您觉得可信吗?反正我信了!什么,您还不信?问我怎么知道 JVM 编译后会优化代码?那要不这样吧,一起来看看 TestClass.java 文件编译后得到的 TestClass.class 文件。为避免说代码是我自己手写的,我决定不粘贴代码,直接截图:

看到了吧,有图有真相,getFinalValue() 方法直接 return "FINAL"!同时也说明了,程序运行时是根据编译后的 .class 来执行的。再不信我我也没办法了哈 : )

顺便提一下,如果您有时间,可以换几个数据类型试试,正如上面说的,有些数据类型是不会优化的。您可以修改数据类型后,根据我的思路试试,看输出觉得不靠谱就直接看 .classs 文件,一眼就能看出来哪些数据类型优化了 ,哪些没有优化。下面说下一个知识点。

想办法也要修改!

不能修改,这您能忍?别着急,不知您发现没,刚才的常量都是在声明时就直接赋值了。您可能会疑惑,常量不都是在声明时赋值吗?不赋值不报错?当然不是啦,事实上,Java 允许我们声明常量时不赋值,但必须在构造函数中赋值。您可能会问我为什么要说这个,这就解释:

我们修改一下 TestClass 类,在声明常量时不赋值,然后添加构造函数并为其赋值,大概看一下修改后的代码(部分代码 ):

public class TestClass {

    //......
    private final String FINAL_VALUE;

    //构造函数内为常量赋值 
    public TestClass(){
        this.FINAL_VALUE = "FINAL";
    }
    //......
}

现在,我们再调用上面贴出的修改常量的方法,发现输出是这样的:

Before Modify:FINAL_VALUE = FINAL
After Modify:FINAL_VALUE = Modified
Actually :FINAL_VALUE = Modified

纳尼,最后一句输出修改后的值了?对,修改成功了!想知道为啥,还得看编译后的 TestClass.class 文件的贴图,图中有标注。

解释一下:我们将赋值放在构造函数中,构造函数是我们运行时 new 对象才会调用的,所以就不会像之前直接为常量赋值那样,在编译阶段将 getFinalValue() 方法优化为返回常量值,而是指向 FINAL_VALUE ,这样我们在运行阶段通过反射修改敞亮的值就有意义啦。但是,看得出来,程序还是有优化的,将构造函数中的赋值语句优化了。再想想那句”程序运行时是根据编译后的 .class 来执行的“,相信您一定明白为什么这么输出了!

请您务必将上面捋清楚了再往下看。接下来再说一种改法,不使用构造函数,也可以成功修改常量的值,但原理上都一样。去掉构造函数,将声明常量的语句改为使用三目表达式赋值:

private final String FINAL_VALUE
        = null == null ? "FINAL" : null;

其实,上述代码等价于直接为 FINAL_VALUE 赋值 “FINAL”,但是他就是可以!至于为什么,您这么想:null == null ? "FINAL" : null 是在运行时刻计算的,在编译时刻不会计算,也就不会被优化,所以你懂得。

总结来说,不管使用构造函数还是三目表达式,根本上都是避免在编译时刻被优化,这样我们通过反射修改常量之后才有意义!好了,这一小部分到此结束!

最后的强调:必须提醒您的是,无论直接为常量赋值通过构造函数为常量赋值 还是 使用三目运算符,实际上我们都能通过反射成功修改常量的值。而我在上面说的修改”成功”与否是指:我们在程序运行阶段通过反射肯定能修改常量值,但是实际执行优化后的 .class 文件时,修改的后值真的起到作用了吗?换句话说,就是编译时是否将常量替换为具体的值了?如果替换了,再怎么修改常量的值都不会影响最终的结果了,不是吗?。其实,您可以直接这么想:反射肯定能修改常量的值,但修改后的值是否有意义?

到底能不能改?

到底能不能改?也就是说反射修改后到底有没有意义?如果您上面看明白了,答案就简单了。俗话说“一千句话不如一张图”,下面允许我用不太规范的流程图直接表达答案哈。注:图中”没法修改”可以理解为”能修改值但没有意义”;”可以修改”是指”能修改值且有意义”。

总结

好的,本次记录就到这儿了,突然不知不觉发现写了好多,这就是我习惯的延伸式学习,喜欢刨根问底 ==|| 我想这篇博客如果您认真的看完,肯定会有收获的!最后,因为内容较多,知识点较多,如果文中有任何错误或欠妥的地方,还望您指正,谢谢!

特别声明:本文经原作者授权转载,未经原作者允许,不得转载其他平台。

原文发布于微信公众号 - 非著名程序员(non-famous-coder)

原文发表时间:2016-05-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏轮子工厂

5. 很“迷”的字符与字符串

最近一直在为自己的浏览量而担忧啦,都快被厂长大人约谈了……我真的有尽力在写稿子哦,所以也请各位老铁,如果觉得我的文章还不错就转发到朋友圈或者微信群之类的,让更多...

12720
来自专栏ShaoYL

iOS循环引用

30650
来自专栏技术总结

iOS不可错过的关键字

建议查看原文:https://www.jianshu.com/p/dce05b24d288(不定时更新)

9430
来自专栏IT探索

g++&&gcc

3.C++:在构造函数中,当使用初始化列表来初始化成员变量时,如果初始化顺序与定义成员变量的顺序不一致,当使用-Wreorder选项时,会重新调整顺序初始化顺序...

7810
来自专栏游戏杂谈

Objective-C 内存管理

1) 手动引用计数 MRC (Mannul Reference Counting);

13010
来自专栏微信公众号:Java团长

Java中的反射机制

反射,当时经常听他们说,自己也看过一些资料,也可能在设计模式中使用过,但是感觉对它没有一个较深入的了解,这次重新学习了一下,感觉还行吧!

10810
来自专栏進无尽的文章

编码篇-iOS程序中的内存分配 栈区堆区全局区等相关知识

在计算机的系统中,运行的应用程序中的数据都是保存在内存中,不同类型的数据,保存的内存区域不同。内存区域大致可以分为:栈区、堆区、全局区(静态区)、文字常量区、程...

22020
来自专栏coolblog.xyz技术专栏

Dubbo 源码分析 - 自适应拓展原理

我在上一篇文章中分析了 Dubbo 的 SPI 机制,Dubbo SPI 是 Dubbo 框架的核心。Dubbo 中的很多拓展都是通过 SPI 机制进行加载的,...

11920
来自专栏曾大稳的博客

c基础

10410
来自专栏java一日一条

一个Java对象到底占用多大内存

大家可以用这个代码边看边验证,注意的是,运行这个程序需要通过javaagent注入Instrumentation,具体可以看原博客。我今天主要是总结下手动计算J...

8910

扫码关注云+社区

领取腾讯云代金券