大家好,又见面了,我是你们的朋友全栈君。
看了好多关于Java反射机制的文章,大多都太过官方,消化起来比较稍显费劲,本篇,我会依据自己的理解去阐述什么是Java的反射机制,反射用在什么地方,以及怎么来使用?
开篇前,我们还是要了解一下,什么是Java的反射机制:
“程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言”。从这个观点看,Perl、Python(看过我写的Python3学习系列的博文,不止一次突出Python动态语言的特点)、Ruby是动态语言,C++、Java、C#不是动态语言。但是JAVA有着一个非常突出的动态相关机制——Reflection(反射),用在Java身上指的是可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体(newInstance)或对其fields设值,或唤起(invoke)其methods方法。
注意 方法的声明和定义不是一回事,
声明:public string Method(string parm1,int param2,…)
定义:public string Method(string parm1,int param2,…)
{
// do something
}
反射用在什么地方?
由于,我们还不清楚反射究竟是什么玩意,怎么用,是不是我们平时写代码的时候会用得上? 这些,都不知道的话,我们也没法定论,这个Java反射机制,用在什么地方比较和合适(注意,一项技术的诞生,一定是为了方便另一项技术的使用,否则会失去本身存在的意义!)
因此,我们先来说一下,反射怎么用?
一、反射的应用
我们可能听过,Java编写的程序,一次编译,到处运行。这也是Java程序为什么是无关平台的所在,原因在于,java的源代码会被编译成.class文件字节码,只要装有Java虚拟机JVM的地方(Java提供了各种不同平台上的虚拟机制,第一步由Java IDE进行源代码编译,得到相应类的字节码.class文件,第二步,Java字节码由JVM执行解释给目标计算机,第三步,目标计算机将结果呈现给我们计算机用户;因此,Java并不是编译机制,而是解释机制),.class文件畅通无阻。
Java的反射机制,操作的就是这个.class文件,首先加载相应类的字节码(运行eclipse的时候,.class文件的字节码会加载到内存中),随后解剖(反射 reflect)出字节码中的构造函数、方法以及变量(字段),或者说是取出,我们先来定义一个类Animal,里面定义一些构造函数,方法,以及变量:
Animal.java:
package com.appleyk.reflect;
public class Animal {
public String name ="Dog";
private int age =30 ;
//默认无参构造函数
public Animal(){
System.out.println("Animal");
}
//带参数的构造函数
public Animal(String name , int age){
System.out.println(name+","+age);
}
//公开 方法 返回类型和参数均有
public String sayName(String name){
return "Hello,"+name;
}
}
我们再定义一个测试类:
ReflectTest.java
package com.appleyk.test;
public class ReflectTest {
public static void main(String args[]) throws Exception{
//do something
}
}
我们运行一下我们的项目,会发现如下:
对应内存中就是:
我们借助javap命令查看一下,这个Animal.class里面的内容是什么:
F:\Java\ReflectClass\bin\com\appleyk\reflect>javap -c Animal.class
Compiled from "Animal.java"
public class com.appleyk.reflect.Animal {
public java.lang.String name;
public com.appleyk.reflect.Animal();
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":
()V
4: aload_0
5: ldc #14 // String Dog
7: putfield #16 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 30
13: putfield #18 // Field age:I
16: getstatic #20 // Field java/lang/System.out:Ljava/
io/PrintStream;
19: ldc #26 // String Animal
21: invokevirtual #28 // Method java/io/PrintStream.printl
n:(Ljava/lang/String;)V
24: return
public com.appleyk.reflect.Animal(java.lang.String, int);
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":
()V
4: aload_0
5: ldc #14 // String Dog
7: putfield #16 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 30
13: putfield #18 // Field age:I
16: getstatic #20 // Field java/lang/System.out:Ljava/
io/PrintStream;
19: new #39 // class java/lang/StringBuilder
22: dup
23: aload_1
24: invokestatic #41 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
27: invokespecial #47 // Method java/lang/StringBuilder."<
init>":(Ljava/lang/String;)V
30: ldc #49 // String ,
32: invokevirtual #51 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
35: iload_2
36: invokevirtual #55 // Method java/lang/StringBuilder.ap
pend:(I)Ljava/lang/StringBuilder;
39: invokevirtual #58 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
42: invokevirtual #28 // Method java/io/PrintStream.printl
n:(Ljava/lang/String;)V
45: return
public java.lang.String sayName(java.lang.String);
Code:
0: new #39 // class java/lang/StringBuilder
3: dup
4: ldc #64 // String Hello,
6: invokespecial #47 // Method java/lang/StringBuilder."<
init>":(Ljava/lang/String;)V
9: aload_1
10: invokevirtual #51 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
13: invokevirtual #58 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
16: areturn
}
我们发现,字节码里面包含了类Animal的构造函数、变量以及方法,但注意,全都是public类型的,我们的定义的类的私有变量 private int age =30 哪去了?当然,既然是类的私有部分,肯定不会暴露在外面的,但是不阻碍我们通过反射获得字节码中的私有成员(本篇只举例说明私有变量(字段field),其他私有类成员同理)。
我们的类Animal在Anima.java中定义,但在Animal.class文件中,我们的Animal类阐述如下:
下面,我们来写一段demo,来演示一下,如何使用反射机制,将.class文件中的类加载出来,并解剖出字节码中对应类的相关内容(构造函数、属性、方法):
看代码前,我们学两个小技巧:
(1)获得类的完全限定名:
copy以后,直接paste
(2)自动生成返回值对象
ReflectTest.java:
package com.appleyk.test;
import java.lang.reflect.Constructor;
import com.appleyk.reflect.Animal;
public class ReflectTest {
public static void main(String args[]) throws Exception{
//do something
//1、加载类 ,指定类的完全限定名:包名+类名
Class c1 = Class.forName("com.appleyk.reflect.Animal");
System.out.println(c1);//打印c1,发现值和字节码中的类的名称一样
//2、解刨(反射)类c1的公开构造函数,且参数为null
Constructor ctor1= c1.getConstructor();
//3、构造函数的用途,就是创建类的对象(实例)的
//除了私有构造函数外(单列模式,禁止通过构造函数创建类的实例,保证一个类只有一个实例)
//ctor1.newInstance()默认生成一个Object对象,我们需要转化成我们要的Animal类对象
// Object a1 = ctor1.newInstance();
Animal a1 = (Animal)ctor1.newInstance();
//4、证明一下a1确实是Animal的实例,我们通过访问类中的变量来证明
System.out.println(a1.name);
}
}
我们看下,上述demo 的执行结果:
我们接着走,获得类中的变量(字段)和方法,两种方式,一个是getXXX,一个是getDeclaredXXX,二者是有区别的,下面demo注释的很详细,并且,我们使用反射出的字段和方法,去获取相应实例的字段值和唤起方法(相当于执行某实例的方法),我们看下完整版demo:
加强版的 ReflectTest.java
package com.appleyk.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import com.appleyk.reflect.Animal;
public class ReflectTest {
public static void main(String args[]) throws Exception {
// do something
System.out.println("A(无参构造函数)--加载类、反射类的构造函数、利用构造函数new一个Animal实例instance--");
// 1、加载类 ,指定类的完全限定名:包名+类名
Class c1 = Class.forName("com.appleyk.reflect.Animal");
System.out.println(c1);// 打印c1,发现值和字节码中的类的名称一样
// 2.a、解刨(反射)类c1的公开构造函数,且参数为null
Constructor ctor1 = c1.getConstructor();
// 3、构造函数的用途,就是创建类的对象(实例)的
// 除了私有构造函数外(单列模式,禁止通过构造函数创建类的实例,保证一个类只有一个实例)
// ctor1.newInstance()默认生成一个Object对象,我们需要转化成我们要的Animal类对象
// Object a1 = ctor1.newInstance();
Animal a1 = (Animal) ctor1.newInstance();
// 4、证明一下a1确实是Animal的实例,我们通过访问类中的变量来证明
System.out.println(a1.name);
System.out.println("A(有参构造函数)--加载类、反射类的构造函数、利用构造函数new一个Animal实例instance--");
// 2.b、 解刨(反射)类c1的公开构造函数,参数为string和int
Constructor ctor2 = c1.getConstructor(String.class, int.class);
Animal a2 = (Animal) ctor2.newInstance("Cat", 20);
System.out.println("B--获得本类中的所有的字段----------------------------");
// 5、获得类中的所有的字段 包括public、private和protected,不包括父类中申明的字段
Field[] fields = c1.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}
System.out.println("C--获得本类中的所有公有的字段,并获得指定对象的字段值-----");
// 6、获得类中的所有的公有字段
fields = c1.getFields();
for (Field field : fields) {
System.out.println(field + ", 字段值 = " + field.get(a1));
// 注意:私有变量值,无法通过field.get(a1)进行获取值
// 通过反射类中的字段name,修改name的值(注意,原值在类中name="Dog")
// 如果,字段名称等于"name",且字段类型为String,我们就修改字段的值,也就是类中变量name的值
if (field.getName() == "name" && field.getType().equals(String.class)) {
String name_new = (String) field.get(a1);// 记得转换一下类型
name_new = "哈士奇";// 重新给name赋值
field.set(a1, name_new);// 设置当前实例a1的name值,使修改后的值生效
}
}
System.out.println("利用反射出的字段,修改字段值,修改后的name = " + a1.name);
System.out.println("D--获取本类中的所有的方法--------------------");
// 7、获取本类中所有的方法 包括public、private和protected,不包括父类中申明的方法
Method[] methods = c1.getDeclaredMethods();
for (Method m : methods) {
System.out.println(m);// 我们在类Animal中只定义了一个public方法,sayName
}
System.out.println("E--获取本类中的所有的公有方法,包括父类中和实现接口中的所有public方法-----------");
// 8、获取类中所有公有方法,包括父类中的和实现接口中的所有public 方法
methods = c1.getMethods();
for (Method m : methods) {
System.out.println(m);// 我们在类Animal中只定义了一个public方法,sayName
}
System.out.println("F--根据方法名称和参数类型获取指定方法,并唤起方法:指定所属对象a1,并给对应参数赋值-----------");
// 9、唤起Method方法(执行) getMethod:第一个参数是方法名,后面跟方法参数的类
Method sayName = c1.getMethod("sayName", String.class);
System.out.println(sayName.invoke(a1, "Tom"));
}
}
我们看下对应的执行结果:
如果,你对上述执行的结果,一次性接收不了的话,建议将上述测试demo自己亲自敲一遍,先别急着一次性敲完,一点点来,按照序号来,你会发现,反射的机制,无非就是先加载对应字节码中的类,然后,根据加载类的信息,一点点的去解剖其中的内容,不管你是public的还是private的,亦或是本类的还是来自原继承关系或者实现接口中的方法,我们java的反射技术 reflect,均可以将其从字节码中拉回到现实,不仅可以得到字段的名字,我们还可以获得字段的值和修改字段的值,不仅可以得到方法的申明我们还可以拿到方法的定义和唤起方法(执行方法),当然,你会有一个这样的疑惑?
为什么new一个对象那么简单,非要用反射技术中的newInstance?
为什么,我可以直接对象a1. 变量访问变量,却非要用反射那么费劲的获得name字段呢?
为什么,我几行代码就能搞定的事情,非要用反射呢?
回到最开始我们讲的地方:
ok,解密答案之前,我们先来思考一个问题?
假设我们定义了很多类,有Animal、Person、Car….. ,如果我想要一个Animal实例,那我就new Animal(),如果另一个人想要一个Person实例,那么他需要new Person(),当然,另一个说,我只要一个Car实例,于是它要new Car()……这样一来就导致,每个用户new的对象需求不相同,因此他们只能修改源代码,并重新编译才能生效。这种将new的对象写死在代码里的方法非常不灵活,因此,为了避免这种情况的方法,Java提供了反射机制,典型的应用如下:
我们知道Spring的IOC吧,即“控制反转”(通过第三方配置文件实现对 对象的控制)。简单说是将我们设计好的对象交给容器控制,而不是直接交给程序内部进行对象的控制。
比如,在Spring中,我们经常看到:
针对上述的配置,我们Spring是怎么帮助我们实例化对象,并放到容器中去了呢? 没错,就是通过反射!!!!
我们看下,下面的伪代码实现过程:
//解析<bean .../>元素的id属性得到该字符串值为"sqlSessionFactory"
String idStr = "sqlSessionFactory";
//解析<bean .../>元素的class属性得到该字符串值为"org.mybatis.spring.SqlSessionFactoryBean"
String classStr = "org.mybatis.spring.SqlSessionFactoryBean";
//利用反射知识,通过classStr获取Class类对象
Class cls = Class.forName(classStr);
//实例化对象
Object obj = cls.newInstance();
//container表示Spring容器
container.put(idStr, obj);
//当一个类里面需要用另一类的对象时,我们继续下面的操作
//解析<property .../>元素的name属性得到该字符串值为“dataSource”
String nameStr = "dataSource";
//解析<property .../>元素的ref属性得到该字符串值为“dataSource”
String refStr = "dataSource";
//生成将要调用setter方法名
String setterName = "set" + nameStr.substring(0, 1).toUpperCase()
+ nameStr.substring(1);
//获取spring容器中名为refStr的Bean,该Bean将会作为传入参数
Object paramBean = container.get(refStr);
//获取setter方法的Method类,此处的cls是刚才反射代码得到的Class对象
Method setter = cls.getMethod(setterName, paramBean.getClass());
//调用invoke()方法,此处的obj是刚才反射代码得到的Object对象
setter.invoke(obj, paramBean);
是不是很熟悉,虽然是伪代码,但是和我们本篇讲的反射机制的使用是相同的,现在知道我们的反射机制用在哪了吧,没错就是我们经常提到的Java web框架中,里面就用到了反射机制,只要在代码或配置文件中看到类的完全限定名(包名+类名),其底层原理基本上使用的就是Java的反射机制。
因此,如果你不做框架的话,基本上是用不到反射机制的,我们大多时候是使用框架的一方,而反射机制都已经在底层实现过了,因此,我们不必担心,我们会写那么复杂的代码。但是,我们必须要理解这种机制的存在!
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/134675.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...