Java反照由浅入深|升级必备版权声明

原创
小哥 3年前 (2022-10-27) 阅读数 3 #大杂烩

这篇博文主要记录了我的学习 Java 反射(reflect在您了解反射之前,您应该首先了解 Java 中的 Class 同学们,如果你们不是很了解,可以先简单了解一下。

一、Java 反射机制

参考了许多博客文章,我总结了以下个人观点。如果有什么不对的地方,我希望改正:

Java 反映机制在程序中。 运行时 对于任何类,您都可以知道该类的所有属性和方法,而对于任何对象,您可以调用其任何方法和属性。这 动态获取信息 以及 动态调用对象的方法。 的函数被调用 java 反思机制

反射机制的一个重要方面是“运行时”,它允许我们在程序运行时的编译过程中加载、探索和使用完全未知的内容。 .class 档案。换句话说,Java 该程序可以加载运行时以知道该名称。 .class 文件,然后学习它的完整结构,并生成它的对象实体或它的 fields(变量)设置一个值,或调用它。 methods(方法)。

我不知道你是否能理解上面的理论。无论如何,当我第一次触摸倒影时,我感到困惑。后来,我写了几个例子:哦~~这就是它的意思!

暂时不懂理论不要紧,先往下看例子,再回头看,相信自己能懂。

2.使用反射获取类的相关信息。

为了使测试结果更加明显,我首先定义了一个 FatherClass 类(默认情况下继承自 Object 类),然后定义从。 FatherClass 类的 SonClass 类,如下所示。可以看出,测试类中变量和方法的访问权限并不是很规范,都是为了更清楚地查看测试结果而刻意设置的。在实际项目中不建议这样做。

FatherClass.java

public class FatherClass {
    public String mFatherName;
    public int mFatherAge;

    public void printFatherMsg(){}
}
复制代码

SonClass.java

public class SonClass extends FatherClass{

    private String mSonName;
    protected int mSonAge;
    public String mSonBirthday;

    public void printSonMsg(){
        System.out.println("Son Msg - name : "
                + mSonName + "; age : " + mSonAge);
    }

    private void setSonName(String name){
        mSonName = name;
    }

    private void setSonAge(int age){
        mSonAge = age;
    }

    private int getSonAge(){
        return mSonAge;
    }

    private String getSonName(){
        return mSonName;
    }
}
复制代码
  1. 获取类的所有变量信息。

    /**

    • 通过反射获取类的所有变量。 */ private static void printFields(){ //1.获取并输出类的名称 Class mClass = SonClass.class; System.out.println("班级名称:" + mClass.getName());

      //2.1 获取所有 public 访问权限的变量 // 包括此类声明的类和从父类继承的类 Field[] fields = mClass.getFields();

      //2.2 获取在此类中声明的所有变量(没有访问权限) //Field[] fields = mClass.getDeclaredFields();

      //3. 遍历变量并输出变量信息 for (Field field : fields) { //获取访问和输出。 int modifiers = field.getModifiers(); System.out.print(Modifier.toString(modifiers) + " "); //输出变量的类型和变量名。 System.out.println(field.getType().getName()

      • " " + field.getName()); } } 复制代码

以上代码注释非常详细,不会对其进行解释。重要的是要注意到,在注释中 2.1 的 getFields() 与 2.2的 getDeclaredFields() 这两种情况之间的区别,下面看看两种情况的输出。请看前面的重点: SonClass extends FatherClass extends Object

  • 调用 getFields() 方法,输出 SonClass 类及其继承的父类。( 包括 FatherClassObject ) 的 public 方法。注: Object 该类中没有成员变量,因此没有输出。

    班级名称:obj.SonClass
    public java.lang.String mSonBirthday
    public java.lang.String mFatherName
    public int mFatherAge
    复制代码
  • 调用 getDeclaredFields() , 输出 SonClass 类的所有成员变量,没有访问权。

    班级名称:obj.SonClass
    private java.lang.String mSonName
    protected int mSonAge
    public java.lang.String mSonBirthday
    复制代码
  1. 获取类的所有方法信息。

    /**

    • 通过反射获取类的所有方法。 */ private static void printMethods(){ //1.获取并输出类的名称 Class mClass = SonClass.class; System.out.println("班级名称:" + mClass.getName());

      //2.1 获取所有 public 访问方法 //包括它们自己的声明和从父类的继承。 Method[] mMethods = mClass.getMethods();

      //2.2 获取此类的所有方法(没有访问权限) //Method[] mMethods = mClass.getDeclaredMethods();

      //3.遍历所有方法 for (Method method : mMethods) { //获取和输出对该方法的访问(Modifiers:修改器) int modifiers = method.getModifiers(); System.out.print(Modifier.toString(modifiers) + " "); //获取并输出该方法的返回值类型。 Class returnType = method.getReturnType(); System.out.print(returnType.getName() + " "

      • method.getName() + "( "); //获取并输出该方法的所有参数。 Parameter[] parameters = method.getParameters(); for (Parameter parameter: parameters) { System.out.print(parameter.getType().getName()
        • " " + parameter.getName() + ","); } //获取并输出该方法引发的异常。 Class[] exceptionTypes = method.getExceptionTypes(); if (exceptionTypes.length == 0){ System.out.println(" )"); } else { for (Class c : exceptionTypes) { System.out.println(" ) throws "
          • c.getName()); } } } } 复制代码

与获取变量信息一样,您需要注意注释。 2.1 与 2.2 不同之处,请看下面的打印输出:

  • 调用 getMethods() 方法 获取 SonClass 类所有 public 访问方法,包括从父类继承的。打印信息中, printSonMsg() 方法来自 SonClass 类, printFatherMsg() 来自 FatherClass 类,其余的方法来自 Object 类。

    班级名称:obj.SonClass
    public void printSonMsg(  )
    public void printFatherMsg(  )
    public final void wait(  ) throws java.lang.InterruptedException
    public final void wait( long arg0,int arg1, ) throws java.lang.InterruptedException
    public final native void wait( long arg0, ) throws java.lang.InterruptedException
    public boolean equals( java.lang.Object arg0, )
    public java.lang.String toString(  )
    public native int hashCode(  )
    public final native java.lang.Class getClass(  )
    public final native void notify(  )
    public final native void notifyAll(  )
    复制代码
  • 调用 getDeclaredMethods() 方法

    在打印的信息中,输出为 SonClass 类的方法不要求访问。

    班级名称:obj.SonClass
    private int getSonAge(  )
    private void setSonAge( int arg0, )
    public void printSonMsg(  )
    private void setSonName( java.lang.String arg0, )
    private java.lang.String getSonName(  )
    复制代码

3.访问或操作类的私有变量和方法。

上面,我们成功地获取了类的变量和方法信息,并在运行时进行了验证 动态获取信息 视野。所以,只是为了获取信息?让我们回顾一下。

每个人都知道对象是私有变量和方法,不能在类上访问或操作,但我们可以通过反射来实现。是的,反思是可以做到的!接下来,让我们探索如何使用反射访问。 类对象的私有方法。 以及修改 私有变量或常量

像往常一样,首先测试类。

注:

  1. 请注意测试类中变量和方法的修饰符(访问权限);
  2. 测试类仅用于测试,在实际开发过程中不提倡这样编写。 : )

TestClass.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;
    }
}
复制代码

3.1 访问私有方法

以访问 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("privateMethod", String.class, int.class);

    //3. 一种启动操作方法
    if (privateMethod != null) {
        //访问私有方法。
        //只是获得访问权限,而不是修改实际权限。
        privateMethod.setAccessible(true);

        //使用 invoke 反射调用私有方法。
        //privateMethod 是否获得了私有方法
        //testClass 要操作的对象
        //后两个参数传递实数参数。
        privateMethod.invoke(testClass, "Java Reflect ", 666);
    }
}
复制代码

值得注意的是,第一个3步中的 setAccessible(true) 方法,是访问私有方法。限,如果不加会报异常 IllegalAccessException 因为当前的方法访问是“private“,详情如下:

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

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

Java Reflect 666
复制代码

3.2 修改私有变量

以修改 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

3.3 修改私有常量

在 3.2 在中,我们介绍了如何修改私有 变量 现在我们来讨论一下如何修改私有 常量

01. 它真的可以被修改吗?

常量是指使用 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 文件已将常量优化为特定值,而特定值直接在运行阶段使用,因此即使修改了常量的值,也没有意义。

下面我们将在测试类中验证这一点。 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 文件中,您可以一目了然地看到哪些数据类型进行了优化。 ,它们都没有经过优化。让我们来谈谈下一个知识点。

02. 想办法修改它吧!

不能修改,你能忍受吗?别担心,我不知道你有没有发现,刚才的常量都是在声明时直接赋值的。您可能会想,常量在声明时不都是赋值的吗?没有任务,就没有错误?当然不是。

方法一

事实上,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 修改后的值真的在文件中起作用吗?换句话说,是否在编译时将常量替换为特定值?如果它被替换了,无论如何修改常量的值,都不会影响最终的结果,对吗?

事实上,你可以直接思考: 反射肯定能修改常量的值,但修改值是否有意义

03. 它可以改变吗?

它可以改变吗?也就是说反射修改后到底有没有意义?

如果你看清楚了,答案很简单。常言道,千言万语不如一图,让我用一个不太规范的流程图来直接表达答案。

注:图中"没法修改"可以理解"可以修改该值,但没有意义";"可以修改"是指"可以修改值并使其有意义"。

四、总结

嗯,这张唱片就在这里。突然,我发现我写了很多东西。谢谢你耐心地听我说。我想如果你仔细阅读这篇博客,你一定会得到一些东西的!最后,因为内容和知识点较多,如果课文中有任何错误或缺陷,请改正。欢迎留言!

作者:伯特
链接:https://juejin.im/post/598ea9116fb9a03c335a99a4
来源:掘金
版权归作者所有。请联系作者以获得商业转载的授权,并注明非商业转载的来源。

版权声明

所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除