Java反射机制

418 阅读13分钟

一、Java 反射机制

什么是类加载器 当Java程序要使用某个类时,如果该类还没有被加载到内存中,则系统会通过加载,链接,初始化这三步来实现对这个类进行初始化。 加载 就是指将class文件读入内存,并为之创建一个Class对象。 任何类被使用时系统都会创建一个Class对象。 链接 验证 是否有正确的内部结构,并和其他类协调一致。 准备 负责为类的静态成员分配内存,并设置默认初始化值。 解析 将累的二进制数据中的符号引用替换为直接引用。 初始化 类的初始化

类的初始化时机 (什么时候会被加载到内存) 创建类的实例 访问类的静态变量,获取为静态变量赋值 使用类的静态方法 使用反射来强制创建某个类或者接口对应得Class对象 初始化某个类的子类(父类先加载到内存中) 使用java.exe命令来运行某个主类 当以上条件满足任意一条时,类就会被初始化

类加载器的职责 负责将.class 文件加载待内存中,并产生Class对象。 类加载器的组成 ①Bootstrap ClassLoader 根类加载器(C代码完成) 负责加载Java运行的核心类 在jre/lib 目录下有个 rt.jar,这个就是系统编译好的核心类库,Bootstrap 就负责加载rt.jar ②Extension ClassLoader 扩展类加载器(C代码完成) 负责加载程序中额扩展应用 扩展应用类在jre/lib/ext目录下 ③System ClassLoader 系统类加载器(Java代码完成) System ClassLoader负责加载我们写的类

什么是反射 Java反射机制是在运行状态中,对于任意一个类,都能知道这个类的所以属性和方法;对于任何一个对象,都能够调用它的任何一个方法和属性;这种 动态的获取信息 以及 动态调用对象的方法 的功能称为 java 的反射机制。 简单来说反射就是解剖一个类,然后获取这个类中的属性和方法,前提是要获取这个类的Class对象。 反射机制很重要的一点就是“运行时”,其使得我们可以在程序运行时加载、探索以及使用编译期间完全未知的 .class 文件。换句话说,Java 程序可以加载一个运行时才得知名称的 .class 文件,然后获悉其完整构造,并生成其对象实体、或对其 fields(变量)设值、或调用其 methods(方法)。

在java中给我们提供了几个这几个类用于描述编译后的各种对象 java.lang.Class 描述编译后的class文件的对象 java.lang.reflect.Constructor 用于描述构造方法 java.lang.reflect.Field 描述字段(成员变量) java.lang.reflect.Method 描述成员方法

如何获取,class文件对象

使用类的对象获取 每个类都使用Object作为父类,Object类方法 getClass() 返回这个类的class文件对象,方法返回值Class类型对象

使用类的静态属性获取 类名.class 返回这个类的class文件对象.属性运行结果也是Class类型对象

使用Class类的静态方法获取 Class类静态方法 forName(String 类名) 传递字符串类名 获取到这个类的class文件对象,方法返回值也是Class类型对象

不管用哪种方式获取的Class对象,他们都是相等的。

        Person p1 = new Person();
        Class cc = p1.getClass();
        System.out.println(cc);


        Class cc2 = Person.class;
        System.out.println(cc2);

        Class cc3 = Class.forName("com.yxm.demo.demo0.Person");
        System.out.println(cc3);

        System.out.println(cc == cc2);
        System.out.println(cc2 == cc3);

结果

class com.yxm.demo.demo0.Person
class com.yxm.demo.demo0.Person
class com.yxm.demo.demo0.Person
true
true

Person类有三个构造函数:

    public Person(){
        System.out.println("Person类无参数构造");
    }
    public Person(int a,int b,String s){
        System.out.println("Person类有参数构造:a:"+a+" b:"+b+" s:"+s);
    }

    private Person(int a){
        System.out.println("Person类有参数   私有 构造:a:"+a);
    }

利用反射获取构造方法

public static void main(String[] args) throws Exception {
        Class clazz = Person.class;

        Constructor[] conarr = clazz.getDeclaredConstructors();

        for (Constructor con : conarr) {
            System.out.println(con);
        }

        Constructor cc = clazz.getConstructor();
        Object oo = cc.newInstance();

        Constructor cc2 = clazz.getConstructor(int.class, int.class, String.class);
        Object oo2 = cc2.newInstance(1, 2, "haha");

        Constructor cc3 = clazz.getDeclaredConstructor(int.class);
        cc3.setAccessible(true);
        Object oo3 = cc3.newInstance(1);

        clazz.newInstance();
    }

输出结果

public com.yxm.demo.demo0.Person()
public com.yxm.demo.demo0.Person(int,int,java.lang.String)
private com.yxm.demo.demo0.Person(int)
Person类无参数构造
Person类有参数构造:a:1 b:2 s:haha
Person类有参数   私有 构造:a:1
Person类无参数构造

二、使用反射获取类的信息

首先定义了一个 FatherClass 类,然后定义一个继承自 FatherClass 类的 SonClass 类,如下所示。

FatherClass.java

public class FatherClass {
    public String fatherName;
    public int fatherAge;

    public void printFatherMsg(){}
}

SonClass.java

public class SonClass extends FatherClass{

    private String sonName;
    protected int sonAge;
    public String sonBirthday;

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

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

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

    private int getSonAge(){
        return sonAge;
    }

    private String getSonName(){
        return sonName;
    }
}

1. 获取类的所有变量信息

/**
 * 通过反射获取类的所有变量
 */
public static void main(String[] args) {
        //1.获取并输出类的名称
        Class sonClass = SonClass.class;
        System.out.println("类的名称:" + sonClass.getName());
        System.out.println("======================================");

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

        //2.2 获取所有本类声明的变量(不问访问权限)
        Field[] fields2 = sonClass.getDeclaredFields();

        //3. 遍历变量并输出变量信息
        System.out.println("getFields()获取变量:");
        goThroughFields(fields);
        System.out.println("======================================");
        System.out.println("getDeclaredFields()获取变量:");
        goThroughFields(fields2);
    }

    private static void goThroughFields(Field[] fields) {
        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 类中没有成员变量,所以没有输出。

    public java.lang.String sonBirthday
    public java.lang.String fatherName
    public int fatherAge
    
  • 调用 getDeclaredFields() , 输出 SonClass 类的所有成员变量,不问访问权限。

    private java.lang.String sonName
    protected int sonAge
    public java.lang.String sonBirthday
    

2. 获取类的所有方法信息

/**
 * 通过反射获取类的所有方法
 */
public static void main(String[] args) {
        //1.获取并输出类的名称
        Class mClass = SonClass.class;
        System.out.println("类的名称:" + mClass.getName());
        System.out.println("======================================");

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

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

        //3.遍历所有方法
        System.out.println("【getMethods()获取方法】");
        goThroughMethods(methods);
        System.out.println("======================================");
        System.out.println("【getDeclaredMethods()获取方法】");
        goThroughMethods(methods2);

    }

    private static void goThroughMethods(Method[] methods) {
        for (Method method : methods) {
            //获取并输出方法的访问权限(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 类。

    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. 取得类中指定方法:    public Method getMethod(String name,Class<?> ... ParameterTypes) throws NoSuchMethodException,SecurityException; 调用方法-->public Object invoke(Object obj,Object args) throws IllegalAccessException,IllegalArgumentException,InvocationTargetException; ##4. 取得指定成员: public Filed getDeclaredFiled(String name) throws NoSuchMethodException,SecurityException;

三、访问或操作类的私有变量和方法

在上面,我们成功获取了类的变量和方法信息,验证了在运行时 动态的获取信息 的观点。那么,仅仅是获取信息吗?

通过对象是无法访问或操作类的私有变量和方法的,但是,通过反射就可以做到。如何利用反射访问 类对象的私有方法 以及修改 私有变量或常量

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;
    }

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

    public String getFinalValue(){
        return FINAL_VALUE;
    }
}

1. 访问私有方法

以访问 TestClass 类中的私有方法 privateMethod(...) 为例

/**
 * 访问对象的私有方法
 * 为简洁代码,在方法上抛出总的异常
 */
private static void main(String[] args) 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 Test", 123);
    }
}

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

Exception in thread "main" java.lang.IllegalAccessException: Class com.yxm.demo.demo2.Test3 can not access a member of class com.yxm.demo.demo2.TestClass with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
	at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
	at java.lang.reflect.Method.invoke(Method.java:491)
	at com.yxm.demo.demo2.Test3.main(Test3.java:29)

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

Java Reflect Test 123

2. 修改私有变量

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

/**
 * 修改对象私有变量的值
 * 为简洁代码,在方法上抛出总的异常
 */
private static void main(String[] args) 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.2 中,我们介绍了如何修改私有 变量,现在来说说如何修改私有 常量

常量是指使用 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 文件已经将常量优化为具体的值,在运行阶段就直接使用具体的值了,所以即使修改了常量的值也已经毫无意义了

/**
 * 修改对象私有常量的值
 */
public static void main(String[] args) 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());
        }
    }

输出:

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

结果出来了:

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

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

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

TestClass.java 文件编译后得到的 TestClass.class 文件截图:

image.png

getFinalValue() 方法直接 return "FINAL"

三、Java 反射库

Java原生提供的反射很是麻烦,使用起来不是不方便。比如我们想要调UserManager的静态方法get,使用原生的实现如下

   Method setMethod = SonClass.class.getDeclaredMethod("setSonName", String.class);
   setMethod.setAccessible(true);
   Method getMethod = SonClass.class.getDeclaredMethod("getSonName");
   getMethod.setAccessible(true);
   setMethod.invoke(obj, "张三");
   System.out.println(getMethod.invoke(obj));

实现起来好不麻烦。这其中

  • 需要确定方法名和参数来获取对应的Method对象
  • 设置Method对象的assessible为true
  • 调用invoke方法,并且传入对应的参数
  • 捕获其中可能抛出来的一连串异常

那么反射能简单点么,当然,而且还会简单很多。

这就是本文想要介绍的,jOOR(Java Object Oriented Reflection),它是一个对java.lang.reflect包的简单封装,使得我们使用起来更加直接和方便。

使用jOOR,上面的代码可以缩短成一行。

Reflect.on(obj).call("setSonName", "李四");
System.out.println(Reflect.on(obj).call("getSonName"));

依赖

API介绍

Reflect

  • Reflect.on 包裹一个类或者对象,表示在这个类或对象上进行反射,类的值可以是Class,也可以是完整的类名(包含包名信息)
  • Reflect.create 用来调用之前的类的构造方法,有两种重载,一种有参数,一种无参数
  • Reflect.call 方法调用,传入方法名和参数,如有返回值还需要调用get
  • Reflect.get 获取(field和method返回)值相关,会进行类型转换,常与call和field组合使用
  • Reflect.field 获取属性值相关,需要调用get获取该值
  • Reflect.set 设置属性相关。

ReflectException

引入ReflectException避免了我们去catch过多的异常,也减少了纵向代码量,使得代码简洁不少。ReflectException抛出,可能是发生了以下异常。

  • ClassNotFoundException
  • IllegalAccessException
  • IllegalArgumentException
  • InstantiationException
  • InvocationTargetException
  • NoSuchMethodException
  • NoSuchFieldException
  • SecurityException

除此之外,ReflectException属于unchecked 异常,语法上不需要显式进行捕获,但是也需要根据实际情况,斟酌是否进行显式捕获该异常。

使用示例

创建实例

String string = Reflect.on(String.class).create("Hello World").get();

访问属性(public,protected,package,private均可)

char pathSeparatorChar = Reflect.on(File.class).create("/sdcard/droidyue.com").field("pathSeparatorChar").get();

修改属性(final属性也可以修改)

String setValue = Reflect.on(File.class).create("/sdcard/drodiyue.com").set("path", "fakepath").get("path");

调用方法(public,protected,package,private均可)

ArrayList arrayList = new ArrayList();
arrayList.add("Hello");
arrayList.add("World");
int value = Reflect.on(arrayList).call("hugeCapacity", 12).get();

实现原理

Reflect实际是对原生java reflect进行封装,屏蔽了无关细节。

以fields方法为例,其内部实现可以看出是调用了java原生提供的反射相关的代码。

public Map<String, Reflect> fields() {
        Map<String, Reflect> result = new LinkedHashMap<String, Reflect>();
        Class<?> t = type();

        do {
            for (Field field : t.getDeclaredFields()) {
                if (type != object ^ Modifier.isStatic(field.getModifiers())) {
                    String name = field.getName();

                    if (!result.containsKey(name))
                        result.put(name, field(name));
                }
            }

            t = t.getSuperclass();
        }
        while (t != null);

        return result;
    }