Java反射机制的原理和用途

看了好多关于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的反射机制

 

 

因此,如果你不做框架的话,基本上是用不到反射机制的,我们大多时候是使用框架的一方,而反射机制都已经在底层实现过了,因此,我们不必担心,我们会写那么复杂的代码。但是,我们必须要理解这种机制的存在!

 

 

 

 

 

 

 

 

©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页