1、反射机制概述以及应用场景
首先,何为反射机制:
JAVA反射机制是在运行状态中,对于任意一个类 (class文件),都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
动态获取类中信息,就是java反射,可以理解为对类的解剖。注意,这里操作的不是“.java”文件,而是字节码文件“.class”。那么反射机制有什么用处?(35-28-01-2.45解析)
要想要对字节码文件进行解剖,必须要有字节码文件对象。 如何获取字节码文件对象呢?(见35-28-01-7.50解析)
这种反射机制大大提高了程序的扩展性,如下Tomcat服务器的例子
注意,配置文件不是往里面写类,而是往里面写我们想要运行的类的信息。(35-day28-02-2.50)配置文件里面配置的其实就是应用程序里面变化的参数信息,而反射就是我们要运行的类是变化的(不确定,因为不同的对象的类有其自己具体的实现),那么我们将这个类写入配置文件。
那么我们如何使用java的反射机制来获取“.class”文件对象呢?(35-day28-02-7.30)java使用Class类来对它的字节码文件“.class”进行描述,Class类提供了一些方法来获取字节码文件当中的信息。
2、反射的使用——获取字节码文件对象的3种方法
先看看下面代码
package reflect.demo;
import test.demo.Person;//注意得将这个包的的Person类导入才可以使用
public class ReflectDemo {
/**
* @param args
* @throws ClassNotFoundException
*/
public static void main(String[] args) throws ClassNotFoundException {
getClassObject_3();
}
/*
* 获取字节码对象的方式:
* 1、Object类中的getClass()方法。
* 想要用这种方式,必须要明确具体的类,并创建对象,较为麻烦
*/
public static void getClassObject_1()
{
//任何对象的创建都要依赖字节码,因此我们可以通过对象来获取其字节码文件对象。
Person p = new Person();
Class clazz = p.getClass();
Person p1 = new Person();
Class clazz1 = p1.getClass();
System.out.println(clazz == clazz1);
/*
结果:
person run
person run
true
创建Person类无参对象,Person的无参构造方法会被调用。
另一方面,不同Person对象所获取的Class对象是相同的,因为2个Person对象的创建都是依赖同一个“.class”文件
*/
}
/*
* 方式二:
* 2、任何数据类型都具备一个静态的属性——.class,利用这个静态属性来获取其对应的Class对象。
* 相对简单,但是还是要明确用到类中的静态成员,还是不够扩展。
*/
public static void getClassObject_2()
{
Class clazz = Person.class;
Class clazz1 = Person.class;
System.out.println(clazz == clazz1);
/*
结果是:true
*/
}
/*
* 方式三:
* 只要通过给定的类的字符串名称就可以获取该类,更为扩展。可用Class类中的forName方法完成。
* 这种方式只要有名称即可,更为方便,扩展性更强。
*/
public static void getClassObject_3() throws ClassNotFoundException
{//这种方式,我们就可以将要实现的类的名称写入配置文件中,而不需要知道这个类是怎么实现的
String className = "test.demo.Person";//先获取类名称的字符串
//static Class<?> forName(String className) 返回与带有给定字符串名的类或接口相关联的 Class 对象。
//系统会通过forName()方法自动在该项目下的“.classpath”文件下寻找,而.classpatn会让其去bin文件查找字符串所指向的类,
//因此这个类必须在当前的项目下,在其他项目下会报出异常
//另外,类名必须带上包名,这样系统才能查找到相应的类,否则查找不到。
//虽然导包,但是导包导入Person类,而我们这里使用的是字符串。我们发现不用导入Person类同样可以执行该程序。
Class clazz = Class.forName(className);
System.out.println(clazz);
/*
结果是:class test.demo.Person
*/
}
}
3、获取Class中的构造函数
我们获取到Person类的.class文件,这与直接new一个Person对象有什么区别?见下面的代码:
package reflect.demo;
import java.lang.reflect.Constructor;
public class ReflectDemo {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
createNewObject_2();
}
//创建空参数的对象
public static void createNewObject() throws ClassNotFoundException, InstantiationException, IllegalAccessException
{
// 早期:new时候,先根据被new的类的名称找寻该类的字节码文件,并加载进内存,
// 并创建该字节码文件对象,并接着创建该字节文件的对应的Person对象
test.demo.Person p = new test.demo.Person();
//现在:
String name = "test.demo.Person";
//根据类的名称,找寻该名称.class文件,并加载进内存,并产生Class对象。
Class clazz = Class.forName(name);
//我们在获取到该类的字节码文件的对象后,如何产生该类的对象呢?——前面new的方式直接在底层获取到类的对象。
// T newInstance()创建此 Class对象所表示的类的一个新实例。因为返回的类型不确定,我们用Object表示
Object obj = clazz.newInstance();
//上面2种方法的效果是一样的,为什么要用下面那种复杂的方法?(35-28-4-8.10解析)
//下面的方法不需要new,只需要将类的名称告诉配置文件即可!
/*
执行结果:
person run
person run
下面的方法同样会初始化Person类
newInstance()方法:创建此 Class对象所表示的类的一个新实例。如同用一个带有一个空参数列表的 new表达式实例化该类。如果该类尚未初始化,则初始化这个类。
*/
}
//创建带参数的对象
public static void createNewObject_2() throws Exception
{
test.demo.Person p = new test.demo.Person("小强",39);
//当获取指定名称对应类中的所体现的对象时,而该对象初始化不使用空参数构造该怎么办呢?
//既然是通过指定的构造 函数进行对象的初始化,所以应该先获取到该构造函数。
//通过字节码文件对象即可完成,该方法是:getConstructor(paramterTypes);
String name = "test.demo.Person";
//同样获取Person类的字节码文件对象
Class clazz = Class.forName(name);
//接下来,获取相应的构造函数的对象
//public Constructor<T> getConstructor(Class<?>... parameterTypes)
//返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。
//parameterTypes参数是 Class对象的一个数组,这些 Class对象按声明顺序标识构造方法的形参类型。
Constructor constructor = clazz.getConstructor(String.class,int.class);
//在Constructor类中,有newInstance()方法
// T newInstance(Object... initargs) 使用此 Constructor对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。
Object obj = constructor.newInstance("小明",38);
//下面的方法,需要给定相应的类的名称,当然因为需要使用带参数的构造函数,还要给定构造函数的参数信息
}
}
其中Person类为
package test.demo;
//首先定义一个类,用于获取其class文件
public class Person
{ //成员变量
public int age;
private String name;
//构造方法
public Person(String name,int age)
{
super();
this.age = age;
this.name = name;
System.out.println("Person param run..."+this.name+":"+this.age);
}
public Person() {
super();
System.out.println("person run");
}
//public类型无参数普通方法
public void show(){
System.out.println(name+"...show run..."+age);
}
//private类型无参数普通方法
private void privateMethod(){
System.out.println(" method run ");
}
//public类型带参数普通方法——在调用的时候外界赋予参数
public void paramMethod(String str,int num){
System.out.println("paramMethod run....."+str+":"+num);
}
//public类型无参数静态方法方法
public static void staticMethod(){
System.out.println(" static method run......");
}
}
4、获取Class中的字段
我们取得字节码文件的构造函数之后,怎么取得它的字段呢?
package reflect.demo;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class ReflectDemo {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception
{
getFieldDemo();
}
public static void getFieldDemo() throws Exception
{
//前面如何获取类中的字段?
// test.demo.Person p = new test.demo.Person();
// p.age = 30;//我们发现这种方法无法访问类中的私有字段
String name = "test.demo.Person";
Class clazz = Class.forName(name);
// Field getField(String name)返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。
//需要注意,这里的String name参数的名称必须与Person类中对应字段的名称一致,否则无法识别
Field field = clazz.getField("age");//这个方法只能获取到public类型的字段,包括Person类的父类的字段——这里我们将age设定为public,name设置为private
//Field getDeclaredField(String name)返回一个 Field 对象,该对象反映此 Class对象所表示的类或接口的指定已声明字段。
Field field_2 = clazz.getDeclaredField("name");//可以获取私有字段
field_2.setAccessible(true);//对私有字段的访问取消权限检查,暴力访问。
Object obj = clazz.newInstance();//同样获取到Person类的对象——此处没有使用带参构造方法对age和name进行初始化,因此下面要先设定age和name的值
//void set(Object obj, Object value) 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
field.set(obj, 66);//将public的age变量设置为新的值
field_2.set(obj, "小林");//将private的name设置为新的值
//Object get(Object obj) 返回指定对象上此 Field 表示的字段的值。
//这里将这个Field所表示的字段封装为Object对象
Object o1 = field.get(obj);
Object o2 = field_2.get(obj);
System.out.println(o1);
System.out.println(o2);
}
}
/*
结果:
person run
66
小林
*/
4、获取Class中的方法
如下代码
package reflect.demo;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class ReflectDemo {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception
{
getMethodDemo_3();
}
//获取指定Class中的所有函数。
public static void getMethodDemo_1() throws Exception
{
Class clazz = Class.forName("test.demo.Person");
//Method[] getMethods() 返回一个包含某些 Method对象的数组,这些对象反映此 Class对象所表示的类或接口
//(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。
Method[] method_1 = clazz.getMethods();
for(Method me:method_1)
{
System.out.println(me);//返回Person类及其父类的public方法
}
System.out.println();
// Method[] getDeclaredMethods()
//返回 Method对象的一个数组,这些对象反映此 Class对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
Method[] method_2 = clazz.getDeclaredMethods();
for(Method me:method_2)
{
System.out.println(me);//返回Person类的所有方法,注意,不获取父类的方法!!!
}
//结果分析:35-28-6-2.05
}
//获取指定Class中的某一个函数
//——需要使用到Person类的成员变量age与name,因此需要使用带参的构造方法对其初始化,但是调用的方法show本身不需要传进参数
public static void getMethodDemo_2() throws Exception
{
Class clazz = Class.forName("test.demo.Person");//同样获取类文件的对象
// Object obj = clazz.newInstance();//通过字节码文件对象,获取Person类的对象
//但是我们发现调用show()方法时,其中的age与name参数需要通过带参构造方法初始化,因此我们必须要创建带参对象
Constructor constructor = clazz.getConstructor(String.class,int.class);
Object obj = constructor.newInstance("康杰",23);//通过构造方法初始化age与name
//public Method getMethod(String name,Class<?>... parameterTypes)
//返回一个 Method对象,它反映此 Class对象所表示的类或接口的指定公共成员方法。name参数是一个 String,用于指定所需方法的简称。
//parameterTypes参数是按声明顺序标识该方法形参类型的 Class对象的一个数组。如果 parameterTypes为 null,则按空数组处理。
//(既获取的方法若没有参数,则 parameterTypes为 null)
Method method_1 = clazz.getMethod("show", null);
//使用Method对象获取到字节码文件对应类的相应方法之后,我们就要对其进行调用。
//Object invoke(Object obj, Object... args) 对带有指定参数的指定对象调用由此 Method对象表示的底层方法。
//因为show方法没有参数,我们调用的时候参数列表为null。注意,将普通方法的参数与构造方法初始化字段的参数区分
method_1.invoke(obj, null);
/*
结果:
Person param run...康杰:23——构造方法
康杰...show run...23——show方法
*/
}
//获取指定Class中的某一个函数
//——不需要使用使用带参的构造方法初始化Person的成员变量,但是调用的方法show本身需要传进参数
public static void getMethodDemo_3() throws Exception
{
Class clazz = Class.forName("test.demo.Person");//同样获取类文件的对象
Object obj = clazz.newInstance();//因此使用的是无参的构造方法,我们直接通过字节码文件对象获取Person的对象,而不需要获取Constructor
//我们想调用paramMethod,其需要传进2个参数——注意传入参数类型的.class。
//——类似于获取带参构造方法的Constructor对象时需要给getConstructor参数
Method method_1 = clazz.getMethod("paramMethod", String.class,int.class);
method_1.invoke(obj, "康杰" , 66);
/*
person run:无参构造方法
paramMethod run.....康杰:66:paramMethod
*/
}
}
5、反射练习
为了完成代码中所说的不用new来完成创建想要使用类的对象,而是只获取其class文件,在内部实现创建对象的动作。我们首先将想要使用的类的名称及其对应的键放入一个pci.properties文件,该文件放在当前项目的路径之下,可以直接在JRE System Library中创建(直接在Test项目中new一个File即可)。
代码如下
PCI类
package reflect.demo;
//创建一个PCI接口
public interface PCI
{
//该PCI接口中有2个抽象方法
public abstract void open();
public abstract void close();
}
-------------
SoundCard类
package reflect.demo;
public class SoundCard implements PCI
{
public void open()
{
System.out.println("sound open");
}
public void close()
{
System.out.println("sound close");
}
}
----------------
NetCard类
package reflect.demo;
public class NetCard implements PCI
{//复写2个方法
public void open()
{
System.out.println("NetCard open");
}
public void close()
{
System.out.println("NetCard close");
}
}
--------------------------
MainBoard类
package reflect.demo;
public class MainBoard
{
public void run()
{
System.out.println("MainBoard run");
}
//创建一个主板的使用PCI及其子类的方法,传入一个PCI对象
public void usePCI(PCI p)//PCI p = new SouncCard();使用的是多态
{
if(p != null)
{
p.open();
p.close();
}
}
}
--------------------------
ReflectTest类
package reflect.demo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
public class ReflectTest {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception
{
//首先,在之前,如果我们想运行网卡类NetCard或者声卡类SoundCard
MainBoard mb = new MainBoard();//首先创建MainBoard类对象
mb.run();
//使用MainBoard类的usePCI方法添加设备,每次添加一个设备都需要修改代码传递一个新创建的对象
// mb.usePCI(new NetCard());
// mb.usePCI(new SoundCard());
//能不能不修改代码就可以完成这个动作,不用new来完成,而是只获取其class文件,在内部实现创建对象的动作。
//首先我们创建pci.properties文件的File对象,并获取该对其的字节读取流
File file = new File("pci.properties");
FileInputStream fis = new FileInputStream(file);
//其次,我们创建Properties类的对象,用于操作pci.properties对象
Properties prop = new Properties();
//将pci.properties文件的读取流加载到prop中
// void load(InputStream inStream) 从输入流中读取属性列表(键和元素对)。
prop.load(fis);//注意这里获取的是键值对
//通过for循环将pci.properties中的内容读出
for(int x=0 ; x<prop.size();x++)
{//获取键对应的值,注意键和值都是字符串。这里值指的就是我们要实现的类的名称
String name = prop.getProperty("pci"+(x+1));
//根据类的名称创建字节码文件对象
Class clazz = Class.forName(name);
//因为声卡类SoundCard与网卡类NetCard都是PCI的子类,我们可以获取他们的对象并强制转换为PCI对象
PCI p = (PCI)clazz.newInstance();
//获取类对应的实例,由于获得的示例是Object,我们可以将其转换为PCI类型,这样就可以作为参数传入mb的usePCI方法
mb.usePCI(p);
}
fis.close();
}
}
//上面这种方法,我们只需要将想要实现的类的名称及其对应的键写入pci.properties配置文件中,
//这样程序便可以自动获取到这个类的对象,并使用该对象。而且,我们想添加新的设备只需要将该设备的名称写入pci.properties文件即可,不需要修改代码,其会自动运行。
----------------
pci.properties文件
pci1=reflect.demo.NetCard
pci2=reflect.demo.SoundCard