前言:鸡和反射
反射是一种反三观的存在,学了反射之后,会发现我们之前所说的private public等权限限制毫无作用,举个不巧当的例子:当你把一只鸡放在砧板上的时候,对你来说这只鸡是毫无隐私的,你想获得什么,就能获得什么,这只鸡就是反射中的.class二进制文件。
一 反射:动态获取类中的信息,就是java反射,可理解为对类的解剖
1 作用是啥?应用场景是啥?
比如说现在有一个应用程序,可以用了,但是现在用户想在这个应用程序的基础上添加自己的需求,对于用户说,拿不到我这个软件的源代码,我这个代码是封装好的,那你客户就不可以随便添加功能,但是用户还想用,这时候怎么办??一般情况下,我这个软件系统可以暴露一些接口,供客户扩展功能。当客户需要某些功能的时候,就要实现这个接口,才能跟我这个软件结合。那问题来了,客户实现了这个接口的类怎么用我这个程序???
方案一:源代码里面new 一个demo
方案一一般不可以在源码里new一个对象,因为客户拿不到应用程序,不可以在里面new对象。
方案二:应用程序对外暴露接口的同时,同时附带上一个配置文件,作为客户,不用了解应用程序的源代码如何,客户只需要将自己实现接口的类的类型放在配置文件里就可以了,那么作为编写软件的程序员来说,怎么做到只根据客户提供的一个类名就来搞事情呢???这里就用到了反射。应用程序中,根据你客户提供的类的名称利用Demo.class来获取你这个类,我如果找到了,就在我的应用程序中加载你的类文件,然后获取里面的所有信息,接着就想干什么就干什么,搞事情。
如果想要对指定名称的字节码文件进行加载并获取其中的内容并调用其中的内容,这就是反射技术能解决的问题,这些东西是在源码里写好的,作为客户来说,你根本就不需要关心应用程序怎么实现的,只需要提供类名即可。
那为什么说是动态的获取类中的信息呢,原因就是在于,应用程序只提供了配置文件供你来放你的类名,而在你将类名传给这个配置文件之前,我这个系统是不知道要处理怎样的类的,只有你客户把类名传进来了,我才能找到你的类文件,这就体现了一种运行时的概念,就是动态获取类中的信息。
图解:
2 反射技术的好处:大大的提高了程序的可扩展耐性。(tomcat大量用到反射)
之前通过多态就已经可以实现可扩展性了,但是多了一个步骤,必须将子类对象赋给父类类型的变量,而在反射中,连子类对象都不用创建了,你直接提供子类的类名就可以了。
在Tomcat中,提供了处理请求和应答的方式,因为具体的处理动作不同,所以对外提供了一个接口,由开发者来实现该接口的类,即实现具体的请求和应答处理,这个接口就叫做Servlet接口,同时,Tomcat还提供了一个配置文件,作为客户(也就是我们这些使用tomcat的开发者),只需要将实现的类放在配置文件中即可,tomcat提供的web.xml配置文件(不可以采用方法一,我们不能在tomcat中new对象),然后在应用程序中就可以利用开发者写的类进行搞事情了。
图解:
3 反射到底是怎么反射的呢???
图解:
左侧为创建实例对象的正常情况,小强是一个对象,产生该对象的类是人;右侧中,在java虚拟机中,所有我们自己编译源程序后产生的.class文件都是一种对象,产生该对象的类是Class,称之为“类” 类,该类就是用来描述字节码文件的,在这个类中提供了一些方法可以操作其实例对象也就是.class文件的一切所有的内容,不管你产生.class文件的源码中设置的是private还是public属性,所有内容都能操作。
4 获取字节码对象的方式
要想对字节码文件进行解剖,必须要有字节码文件对象。如何获取字节码文件对象呢???
方法一:Object类中的getClass方法。
Person p=new Person();
Class clazz=p.getClass(); //这就获取了Person 类对象p的.class文件
Person p1=new Person();
Class clazz1=p1.getClass();
System.out.println(clazz==clazz1);//为什么输出是true??? 因为p和p1都是依赖于同一个类完成的
System.out.println(p.getClass());
System.out.println(p1.getClass()); //两个输出都是一样的,都是Person类
方法二:任何数据类型都具备一个静态的属性 .class 来获取其对应的Class对象,这个时候就绕过了new 构造函数的方式(只是说绕过了new 构造函数,不是说底层不再使用构造函数),方法相对简单,但是还是要明确用到类名,使用了类中的静态成员,不够扩展。
Class clazz=Person.class;
System.out.println(clazz); //输出Person
方法三:重点掌握 只要通过给定的类的字符串名称就可以获取该类的字节码文件.class,此时更为扩展。可以使用Class类中的方法完成。该方法就是Class.forName()方法。这里更为扩展的原因就是,将类名写成了字符串,而不是一种类型了,可以将这个字符串放在配置文件中,就完事了,这种方式更为方便,扩展性更强。这里也是绕过了new 构造函数的方式,对于源文件,javac编译之后就产生了.class,此时还没有运行,并未产生类对象,也没有new对象。
//创建了一个Person类,如果写成下面这种方式,是不正确的,或者有时候是不正确的,因为没有带上包名。
System.out.println(Class.forName("Person"));
// 正确的方式:
Class clazz=Class.forName("com.hainiu.archiver.Person");
System.out.println(clazz);
5 如何利用获取的字节码文件.class 文件来创建对象(代码中的注释都是精髓)
public static void createNewObject() throws Exception{
//早期,new 的时候,先根据被new的类的名称找寻给类的字节码文件,并加载进内存
//并创建该字节码文件对象,并接着创建该字节文件的对应的Person对象。
//Person p=new Person(); //通过构造函数进行创建对象
//现在
String name="com.hainiu.archiver.Person";
//1 找寻该文件,并加载进内存,并产生Class对象
Class clazz=Class.forName(name); //只是找到.class的字节码文件
//2 利用字节码文件产生对象
Object obj=clazz.newInstance(); //这三个步骤就相当于上面new对象的一步,由于将其拆开,并且后面两步创建对象的过程是由Class类完成的
//而对于new对象来说,必须是我自己new一次,相比更复杂了,且没有扩展性。
//这个newInstance()跟new一个实例对象是一样的,也会调用类中的空参构造函数。那如何调用有参的构造函数呢
//一般被反射的类都具有空参,因为,Class中只有一个newInstance方法,且无参,所以比较方便
//那怎么反射有参的构造函数???
//也就是说此时是要通过指定的构造函数,所以应该先获取到该构造函数
//getConstructors(); 返回的是构造函数对象,体现了java中万物皆对象的思想,甚至字段、方法都是对象
Constructor constructor=clazz.getConstructor(int.class,String.class);
Person p=(Person)constructor.newInstance(34,"gtl"); //这样写不好,没有扩展性,因为还是用到了Person类的类名,这样不好,不应该出现类的类名
System.out.println(p.getAge()); //这里要想使用p.中的getAge方法,仍然用到了类型Person,这样扩展性就小了,事实上作为开发人员,在客户没有传参进来之前是不可能知道客户创建了什么样的类的,所以,这里能这样写
//要想调用getAge方法,应该用下面这个方式:这种方式就不会设计到具体的类名。
//首先获取方法
Method m=clazz.getMethod("方法名", 参数类型.class, 参数类型2.class,....); //作为开发者,这个方法名实际上是我接口中定义好的,所以可以写死
//然后调用方法
m.invoke(obj, "参数"); //空参:m.invoke(obj,null); //作为开发者,这个参数是客户传进来的,是字符串类型,应该写在xml文件里,这样开发者就可以用了。
}
}
上述代码的最后一段,个人觉得是比较容易犯的错误,也是类反射的经典所在。
6 通过字节码文件获取字段 getField(String name) :字段也是对象
public static void getFieldDemo() throws Exception{
//这里不管你字段是私有的还是公有的,我都能拿的着,用getDeclaredField
//对于私有的,用getField方式是拿不着的,
Class clazz=Class.forName("com.hainiu.archiver.Person");
Field field=clazz.getDeclaredField("age");
System.out.println(field);
//用getDeclaredField()可以拿到类中所有内容,包含私有的
//对私有字段的访问取消权限检查
field.setAccessible(true); //暴力访问,如果不加这句话,仍然不能访问私有的
Object obj=clazz.newInstance();
field.set(obj, 89);//重新设置field的对象
Object o=field.get(obj); //表示获取字段是哪个对象的
System.out.println(o);
}
7 通过字节码文件获取方法 getMethod() :方法也是对象
/**
* 获取指定Class中的公共函数
*
*/
public static void getMethodDemo() throws Exception {
Class clzz=Class.forName("com.hainiu.archiver.Person");
Method[] methods=clzz.getMethods();//获取的是公有方法
//获取私有方法:getDeclaredMethods();//获取本类中所有的私有方法
for(Method i:methods){
System.out.println(i);
}
//获取一个方法
Method method= clzz.getMethod("show",null);//第一个参数为方法名,第二个参数为该方法的参数
//运行该方法
// Object obj=clzz.newInstance();
Constructor constructor=clzz.getConstructor(String.class,int.class);//调用有参构造函数
Object obj=constructor.newInstance("小明","34");
method.invoke(obj, null);//运行该无参方法;
}