一、反射是什么?
Java反射是Java提供的一种机制,其允许程序可以在运行时操作类或者对象的内部属性。通过这种机制,Java程序可以动态地创建对象、调用对象的方法、改变对象中的字段值等。
二、如何使用反射?
1.获取Class对象
Java反射的关键是Class类,这个特殊的类可以访问程序运行中的所有对象的所属类的所有信息。获取Class对象有三种方法:
第一种为对象.getClass()
,这种方式要求你已经有一个类的实例对象,如下所示。注意getClass()方法的返回值是一个Class泛型对象,如果对泛型还不熟悉,可以看这篇博客补充相关知识:https://blog.csdn.net/m0_46421541/article/details/136059347。结果是study.Person,其中study是我自己设置的包名,Person是该对象的所属类。
你可能会想,直接把aClass实例的泛型固定成Person可不可以?如下所示,这样的代码会在编译阶段报错。原因如下:我们都知道java有多态机制,person变量可能真正引用的不一定是person对象(虽然在示例代码中确实是引用person对象,但要考虑所有可能发生的情况),也就是说person变量可能指向Person的子类对象,或Person的子类的子类对象,那么person变量到底指向的是哪个对象需要等到程序运行时才能确定。同时,反射这个行为也是在程序运行时进行了,也就是aClass这个对象到底是保存的哪个类的信息只有在运行阶段才知道,在编译阶段不知道。因此在编译阶段,我们就仅仅知道person变量指向的对象的所属类一定是Person或Person的子类,所以使用通配符上界<? extends Person>
来表示这个类型范围。
第二种方式为类字面常量类.class
,如下所示,注意,这种方式生成Class对象并不会进行类的加载,因此类种的静态资源都没有加载(这块只有在你访问类静态成员或创建类的实例时才被加载)。同时,与第一种方式不同,此方法的返回类型的泛型可以确定是Person,这是因为我们是直接通过类来获取Class对象,而第一种方式是通过实例对象来获取Class对象。
第三种方法为Class.forName
,如下所示。这种方式通常用于类名在编译时不可知的情况(例如类名存放在文件中,在程序运行时才能从文件中读取,在编译阶段不知道类名)。同时注意以下细节:使用该方法需要传入类的全路径,包括包名和类名;使用该方法需要抛出一个ClassNotFoundException异常;使用该方法产生Class对象时,会立即触发类的初始化,类的静态块会被执行,如下所示。
注意,JVM会为每个类管理唯一的Class对象,因此上述三种方法得到的Class对象其实都是同一个,如下所示:
2.反射操作类成员变量
1)getFields
,该方法获取类中的所有public修饰的属性,返回是一个Field数组,同时注意,从父类中继承的共有方法也能获得,如下所示:
2)getDeclareFields
,该方法返回类中的所有属性,包括private修饰的,但不包括从父类继承的属性,如下所示:
如果我们想获得父类的所有字段,包括private修饰的,应该怎么办呢?可以先使用getSuperclass
获取父类的Class对象,再调用父类Class对象的getDeclaredFields
方法,如下所示:
这里有个规律就是getFields,包括后面提到的getMethods,getConstructorst等方法只能分别获取public修饰的(包括从父类继承的)属性,方法,构造器和注解,而包含“Declared”的方法如getDeclaredFields,getDeclaredMethods,getDeclaredConstructors等可以获取该类除了从父类中继承的以外的所有属性,方法和构造器。
3)getDeclaredField,获取指定变量名的属性,并可通过getType
方法获得属性的的Class对象,如下所示:
如果成员属性是一个泛型,getType方法只会返回泛型擦除后的类型,getGenericType
方法可以获得泛型类型,如下所示:
)
如果想要获取并修改类静态属性的值,需要注意该静态属性是否是private修饰的,如果是,需要加上,使用get和set可以获取和修改静态变量的值,因为是静态属性,不属于哪个具体的对象,因此get的参数为null,如下所示:
但是如果该静态属性被final关键词修饰,则不能使用set方法修改,不然会在运行阶段报错,如下所示:
如果是非静态的成员属性,也可以获得并修改其public或private修饰的成员属性,和静态成员属性不同的是,连final修饰的属性一样也能修改,如下所示:
4) getModifiers,该方法的返回值是一个整数,代表这不同的访问权限,public是1,private是2,如下所示,如果觉得不好记,可以使用Modifier.toString
打印出具体的访问权限关键字,注意默认的访问权限的打印结果是空字符串。
3.反射操作类成员方法
获取类成员方法的方式和获取类成员属性的过程类似,
1)getMethods,该方法获得类中所有public修饰的方法,包括从父类中继承的,如下所示:
2)getDeclaredMethods,该方法获得类中所有的方法,但不包括从父类中继承的,如下所示:
3) getDeclaredMethod,需要输入获得的方法名,如下所示
如果我们想要获取某个带参数的方法,那么需要在往getDeclaredMethod方法中传入参数的类型,如下所示:
4)invoke,该方法可以调用类中的方法,如果是静态方法,则第一个参数传入的参数为null。若调用的方法有参数,也可传进invoke中,如下所示:
如果是非静态方法,则需要传入对象,如下所示:
4) getModifiers,和属性中的操作类似,该方法可以返回成员方法的访问权限,是一个数字代表的,public是1,private是2,Modifier.toString
方法可以用于打印数字对应的访问权限关键字,如下所示:
3.反射操作构造方法
使用getDeclaredConstructor或getConstructor都可以获得类的构造方法,这俩的区别和属性以及方法中一样,因此不再赘述。getDeclaredConstructor不传入任何参数表示获得无参构造器,若想获得有参构造器,则需要传入对应形参的所属类的Class对象。同时,调用newInstance方法可以获得一个对象(Object类型的),强转成Person类即可使用Person类的成员方法,如下所示;
这种将生成的对象强转成已知对象的方法在实际的反射程序中很少使用,因为如果我们在编译阶段都知道要使用哪个类了,那还用啥发射生成对象,直接new一个对象就行了。那你就会问了,newInstance返回的是一个Object类型的对象,它里面根本没有say()方法,我不强转,又怎么调用person对象中的say方法呢?其实,使用Method对象.invoke
方法即可,如下所示。
三、Java反射原理
Person类的Class对象为什么能访问到Person类的所有信息(属性、成员方法和构造器等)的呢?这需要理解程序的运行过程。在编译阶段,编译器会将我们编写的以.java
结尾的java源代码文件编译成以.class
结尾的字节码文件。这里注意一点,类的所有信息,例如类名,父类,属性,方法,注解等,不会在编译阶段丢失。接下来JVM会利用Class Loader(类加载器)来加载字节码文件中的内容,并自动的将类的所有信息保存在一个Class对象中。因为每个类只会被JVM加载一次,因此每个类都对应着唯一的一个Class对象。
因此,我们之所以可以通过反射获取到类的所有信息,是因为类的所有信息被JVM保存在一个唯一的Class对象中,通过那个唯一的Class对象,我们可以反过来创建对象,调用类方法,访问类成员属性等。因此,反射的本质就是利用某个具体类的Class对象来获取该类的各种信息。
四、为什么需要反射?
如果在编写阶段已经明确了要使用的数据类型,那么直接显示的new一个对象更加方便,而不必依赖于反射。反射的真正价值在于处于在可以在程序运行时创建类,调用类方法。如果我们需要从数据库中获取的需要创建的类名和需要调用的方法名时,此时就可以使用反射来编写更加通用的代码。
Java反射的常见应用:
- 框架和库开发:反射在框架和库的开发中非常有用。例如,Spring框架使用反射实现依赖注入和AOP(面向切面编程)。在这些场景中,反射允许框架在运行时动态地加载和处理类,从而提供了极大的灵活性。
- 动态代理:在动态代理模式中,反射被用来在运行时动态地创建代理对象。这允许开发者在不修改原始类的代码的情况下,为对象添加新的功能,如日志记录、性能监控或事务管理等。
- 测试和调试:反射在测试和调试中也很有用。通过反射,开发者可以在运行时检查对象的状态,包括私有字段的值。这对于编写断言和验证程序的正确性非常有帮助。
- 序列化和反序列化:在序列化和反序列化的过程中,反射被用来获取类的信息,如字段名、字段类型和字段值等。这使得开发者可以将对象转换为字节流,并在需要时将其恢复为对象。
- 动态加载和扩展:反射允许程序在运行时动态地加载和扩展类。这对于实现插件化架构或构建可扩展的应用程序非常有用。例如,可以根据用户输入的类名或配置文件中的类名来动态加载类,以实现扩展功能。
- 访问和修改私有成员:虽然通常不推荐这样做,但在某些特殊情况下,反射可以被用来访问和修改类的私有成员。这提供了对类内部状态的完全控制,但也可能破坏封装性和安全性。
总结
反射允许程序在运行时动态地加载和操作类,而不需要在编译时知道所有的类信息。反射可以用来实现框架和库,这些框架和库需要在运行时动态地加载和处理类。例如,Spring框架就广泛使用了反射来实现依赖注入和AOP(面向切面编程)。反射可以用来在运行时检查对象的状态,这对于调试和测试非常有用。反射操作通常会比直接操作更慢,因此在性能敏感的场景中应谨慎使用。总的来说,Java反射是一项强大的功能,它提供了很大的灵活性和扩展性。然而,由于其潜在的安全性和性能问题,以及可能对封装性的破坏,因此在使用反射时需要谨慎并仔细考虑其影响和后果。