? Java 反射
1. 什么是 Java 的反射机制
反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。
⭐ 在 Java 运行时环境中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
Java 反射机制主要提供了以下功能:
- 在运行时(动态编译)判断任意一个对象所属的类。
- 在运行时构造任意一个类的对象。
- 在运行时判断任意一个类所具有的成员变量和方法。
- 在运行时调用任意一个对象的方法和属性。
这种动态获取信息以及动态调用对象的方法的功能称为 Java 语言的反射机制
2. Class 类
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。 这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。
可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class
,Class
类用于表示 .class
文件(字节码。
说的通俗点,反射就是把 Java 类中的各种成分映射成一个个的 Java 对象。
例如一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
如图是类的正常加载过程:
⭐ 将 .class
文件读入内存的时候,JVM 会为之创建一个 Class
对象
3. 获取 Class 类对象的四种方式
Class
类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了四种方式获取 Class
对象:
? 1. 知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class;
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
? 2. 通过 Class.forName()
传入类的路径获取:
Class alunbarClass1 = Class.forName("com.xxx.TargetObject");
Class.forName(className)
方法,内部实际调用的是一个 native 方法 forName0(className, true, ClassLoader.getClassLoader(caller), caller)
;
第 2 个 boolean
参数表示类是否需要初始化,Class.forName(className)
默认是需要初始化。
一旦初始化,就会触发目标对象的 static
块代码执行,static
参数也会被再次初始化。
? 3. 通过对象实例 instance.getClass()
获取:
Employee e = new Employee();
Class alunbarClass2 = e.getClass(); // 获取该对象实例的 Class 类对象
? 4. 通过类加载器 xxxClassLoader.loadClass()
传入类路径获取
class clazz = ClassLoader.LoadClass("com.xxx.TargetObject");
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行
虚拟机为每个类型管理一个 Class 对象。 因此,可以利用 ==
运算符实现两个类对象比较的操作。 例如,
if(e.getClass() == Employee.getClass())
4. 动态地创建一个类的实例
① 使用 class.newInstance
⭐ 还有一个很有用的方法 newInstance()
, 可以用来动态地创建一个类的实例。例如,
e.getClass().newInstance();
创建了一个与 e
具有相同类类型的实例。 ? newlnstance
方法调用默认的构造函数(无参构造函数)初始化新创建的对象。如果这个类没有默认的构造函数, 就会抛出一个异常
将 forName
与 newlnstance
配合起来使用, 可以根据存储在字符串中的类名创建一个对象 :
String s = "java.util.Random";
Object m = Class.forName(s).newInstance();
② 使用开源库 Objenesis
Objenesis 是一个Java的库,主要用来创建特定的对象。
由于不是所有的类都有无参构造器又或者类构造器是 private,在这样的情况下,如果我们还想实例化对象,class.newInstance
是无法满足的。
public class Test {
private int i;
public Test(int i){
this.i = i;
}
public void show(){
System.out.println("test..." + i);
}
}
------------------------
public static void main(String[] args) {
Objenesis objenesis = new ObjenesisStd(true);
Test test = objenesis.newInstance(Test.class);
test.show();
}
使用非常简单,Objenesis
由子类 ObjenesisObjenesisStd
实现,构造方法中使用了缓存
public ObjenesisStd(boolean useCache) {
super(new StdInstantiatorStrategy(), useCache);
}
详细源码此处就不深究了,了解即可。
5. 通过反射获取构造方法并调用
批量获取:
public Constructor[] getConstructors()
:获取所有"公有的"构造方法public Constructor[] getDeclaredConstructors()
:获取所有的构造方法(包括私有、受保护、默认、公有)
单个获取:
public Constructor getConstructor(Class... parameterTypes)
: 获取一个指定参数类型的"公有的"构造方法:public Constructor getDeclaredConstructor(Class... parameterTypes)
: 获取一个指定参数类型"构造方法",可以是私有的,或受保护、默认、公有;
举个例子:
package fanshe;
public class Student {
//---------------构造方法-------------------
//(默认的构造方法)
Student(String str){
System.out.println("(默认)的构造方法 s = " + str);
}
// 无参构造方法
public Student(){
System.out.println("调用了公有、无参构造方法执行了。。。");
}
// 有一个参数的构造方法
public Student(char name){
System.out.println("姓名:" + name);
}
// 有多个参数的构造方法
public Student(String name ,int age){
System.out.println("姓名:"+name+"年龄:"+ age);//这的执行效率有问题,以后解决。
}
// 受保护的构造方法
protected Student(boolean n){
System.out.println("受保护的构造方法 n = " + n);
}
// 私有构造方法
private Student(int age){
System.out.println("私有的构造方法 年龄:"+ age);
}
}
----------------------------------
package fanshe;
import java.lang.reflect.Constructor;
public class Constructors {
public static void main(String[] args) throws Exception {
// 加载Class对象
Class clazz = Class.forName("fanshe.Student");
// 获取所有公有构造方法
Constructor[] conArray = clazz.getConstructors();
for(Constructor c : conArray){
System.out.println(c);
}
// 获取所有的构造方法(包括:私有、受保护、默认、公有)
conArray = clazz.getDeclaredConstructors();
for(Constructor c : conArray){
System.out.println(c);
}
// 获取公有、无参的构造方法
// 因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
// 返回的是描述这个无参构造函数的类对象。
Constructor con = clazz.getConstructor(null);
System.out.println("con = " + con);
//调用构造方法
Object obj = con.newInstance();
// System.out.println("obj = " + obj);
// Student stu = (Student)obj;
// 获取私有构造方法
con = clazz.getDeclaredConstructor(char.class);
System.out.println(con);
//调用构造方法
con.setAccessible(true); // 为了调用 private 方法/域 我们需要取消安全检查
obj = con.newInstance('男');
}
}
6. 通过反射获取成员变量并使用
批量获取:
public Field[] getFields()
:获取所有公有的字段public Field[] getDeclaredFields()
:获取所有的字段 (包括私有、受保护、默认的)
单个获取:
public Field getField(String name)
: 获取一个指定名称的公有的字段public Field getDeclaredField(String name)
: 获取一个指定名称的字段,可以是私有、受保护、默认的
举个例子:
package fanshe.field;
public class Student {
public Student(){
}
//**********字段*************//
public String name;
protected int age;
char sex;
private String phoneNum;
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + ", sex=" + sex
+ ", phoneNum=" + phoneNum + "]";
}
}
----------------------------------
package fanshe.field;
import java.lang.reflect.Field;
public class Fields {
public static void main(String[] args) throws Exception {
// 获取Class对象
Class stuClass = Class.forName("fanshe.field.Student");
// 获取所有公有的字段
Field[] fieldArray = stuClass.getFields();
for(Field f : fieldArray){
System.out.println(f);
}
// 获取所有的字段(包括私有、受保护、默认的)
fieldArray = stuClass.getDeclaredFields();
for(Field f : fieldArray){
System.out.println(f);
}
// 获取指定名称的公有字段
Field f = stuClass.getField("name");
System.out.println(f);
Object obj = stuClass.getConstructor().newInstance(); // 获取该对象的实例
// 为字段设置值
f.set(obj, "刘德华"); // 为Student对象中的name属性赋值
// 获取私有字段
f = stuClass.getDeclaredField("phoneNum");
System.out.println(f);
f.setAccessible(true); //暴力反射,解除私有限定
f.set(obj, "18888889999");
}
}
7. 通过反射获取成员方法并调用
批量获取:
public Method[] getMethods()
:获取所有"公有方法"(包含了父类的方法也包含 Object 类)public Method[] getDeclaredMethods()
: 获取所有的成员方法,包括私有的(不包括继承的)
单个获取:
public Method getMethod(String name, Class<?>... parameterTypes)
: 获取一个指定方法名和参数类型的成员方法:
调用方法:
Object invoke(Object obj, Object... args)
:obj 要调用方法的对象;args 调用方法时所传递的实参
举个例子:
package fanshe.method;
public class Student {
public void show1(String s){
System.out.println("调用了:公有的,String参数的show1(): s = " + s);
}
protected void show2(){
System.out.println("调用了:受保护的,无参的show2()");
}
void show3(){
System.out.println("调用了:默认的,无参的show3()");
}
private String show4(int age){
System.out.println("调用了,私有的,并且有返回值的,int参数的show4(): age = " + age);
return "abcd";
}
}
-------------------------------------------
package fanshe.method;
import java.lang.reflect.Method;
public class MethodClass {
public static void main(String[] args) throws Exception {
// 获取 Class对象
Class stuClass = Class.forName("fanshe.method.Student");
// 获取所有公有方法
stuClass.getMethods();
Method[] methodArray = stuClass.getMethods();
for(Method m : methodArray){
System.out.println(m);
}
// 获取所有的方法,包括私有的
methodArray = stuClass.getDeclaredMethods();
for(Method m : methodArray){
System.out.println(m);
}
// 获取公有的show1()方法
Method m = stuClass.getMethod("show1", String.class);
System.out.println(m);
Object obj = stuClass.getConstructor().newInstance(); // 实例化一个Student对象
m.invoke(obj, "刘德华");
// 获取私有的show4()方法
m = stuClass.getDeclaredMethod("show4", int.class);
System.out.println(m);
m.setAccessible(true); //解除私有限定
Object result = m.invoke(obj, 20); //需要两个参数,一个是要调用的对象(获取有反射),一个是实参
System.out.println("返回值:" + result);
}
}
8. 通过反射获取配置文件内容
student 类:
public class Student {
public void show(){
System.out.println("is show()");
}
}
配置文件以 txt 文件为例子(pro.txt):
className = cn.fanshe.Student
methodName = show
测试类:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;
/*
* 我们利用反射和配置文件,可以使:应用程序更新时,对源码无需进行任何修改
* 我们只需要将新类发送给客户端,并修改配置文件即可
*/
public class Demo {
public static void main(String[] args) throws Exception {
//通过反射获取Class对象
Class stuClass = Class.forName(getValue("className"));//"cn.fanshe.Student"
//2获取show()方法
Method m = stuClass.getMethod(getValue("methodName"));//show
//3.调用show()方法
m.invoke(stuClass.getConstructor().newInstance());
}
//此方法接收一个key,在配置文件中获取相应的value
public static String getValue(String key) throws IOException{
Properties pro = new Properties();//获取配置文件的对象
FileReader in = new FileReader("pro.txt");//获取输入流
pro.load(in);//将流加载到配置文件对象中
in.close();
return pro.getProperty(key);//返回根据key获取的value值
}
}
当我们不要 Student 类,而需要新写一个 Student2 的类时,这时只需要更改 pro.txt
的文件内容就可以了。代码就一点不用改动。配置文件更改为:
className = cn.fanshe.Student2
methodName = show2
9. 反射机制优缺点
优点: 动态编译。运行期类型的判断,动态加载类,提高代码灵活度。
缺点:
- 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。
- 安全问题:让我们可以动态操作改变类的属性同时也增加了类的安全隐患。
10. 反射的应用场景
反射是框架设计的灵魂。
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
举例:
- 我们在使用 JDBC 连接数据库时使用
Class.forName()
通过反射加载数据库的驱动程序 - Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系
- 动态配置实例的属性
? References
- 《Java 核心技术 - 卷 1 基础知识 - 第 10 版》
- 《Thinking In Java(Java 编程思想)- 第 4 版》
- 敬业的小马哥 — Java基础之—反射(非常重要)
注意:本文归作者所有,未经作者允许,不得转载