一般来说,只有在编写框架的时候才会用到反射技术,不编写框架的话,是用不到他的,但是可以帮助我们更好的学习和适用框架。
一个类有多个组成部分,例如:成员变量,方法,构造方法等等,反射就是加载类,并解析出类的各个组成部分。
这里面的两个部分,加载类 和 解析类:
一、加载类
Java中有一个Class类,看准了这个class的名字叫Class,这个Class类用于代表某一个类的字节码。
这个类里就提供了加载某个类的字节码的方法:forName()。forName方法用于加载某个类的字节码到内存中,并使用class对象进行封装。调用这个forName的方法,就加载出了类,完成了加载类这一步的功能,就是反射的第一步。
比如我们写一个测试类来测试一下forName的方法,旁边有之前的类作为被测试的对象:
public static void main(String[] args) throws ClassNotFoundException {
Class class1=Class.forName("jUnit.User");
}
用了一个叫class1的Class类保存了user类的字节码,将整个类加载到内存里了,需要注意的是forName的参数,是加载类的完整路径,一般只要到上一级目录就可以,如果报了ClassNotFound异常,可以去检查这个路径。
除了这种方法,加载类还有几种其他的方法:
一种是在new出一个类的实例的时候,调用getClass方法:
Class class2=new User().getClass();
还有一种:
Class class3=User.class;
二、解析类(或者叫反射类)
加载完成之后的下一步是解析类(反射类,反射是动词),同样Class类提供了一些常用方法**getConstructor、getMethod、getField来从类中解剖(反射)出构造函数,方法和成员变量,前提是这个类里面的成员都是public的,如果是私有的,那么用这三个方法是访问不到的,那么还有另外三个方法getDeclaredConstructor、getDeclaredMethod、getDeclaredField**。
用这些方法,获取到了想要的类的内部的各种成员,接下来是为了干啥呢
或者说,这些构造器,方法,成员变量的作用是什么,解析出构造器,就可以用这些构造器来创建对象了,也就是说,其实能够为了更好的利用他们。
接下来我们用实例来看一下怎么解析类的具体做法。
2.1反射类的构造函数
方便操作,我们先重新写一个类,然后写好里面的构造器
/*
* 一些重载的构造器
*/
public Person() {
// TODO Auto-generated constructor stub
System.out.println("person name");
}
public Person(String name) {
System.out.println("person name");
}
public Person(String name,int password) {
System.out.println("person name : password");
}
private Person(List list) {
System.out.println("list");
}
然后我们测试去解析这些构造器并创建对象:
//第一步肯定要先加载进来
Class class1=Class.forName("reflect.Person");
//然后反射第一个方法public Person(),赋给一个构造器对象
Constructor constructor1=class1.getConstructor(null);
现在一个对象constructor1拿到了,那么他必定会有了Person类的构造函数了,我们就可以用这个对象去创建person实例:
Person person=(Person) constructor1.newInstance(null);
一般都要强转类型,因为newInstance方法在设计的时候统一返回值肯定都是Object类
这时候我们把这个测试方法用Junit测试一下,就会发现已经输出了:
这是因为反射第一个构造器的时候,构造器会输出一句person。
我们为什么不直接new一个对象,然后自动调用无参构造器,而写这么多麻烦的代码呢,再次强调,这是反射机制,这是在写框架的时候才会用到的。
再来反射我们Person类里面的另外几个构造器:
@Test
public void test2() throws Exception {
Class class2=Class.forName("reflect.Person");
Constructor constructor2=class2.getConstructor(String.class);
Person person2=(Person) constructor2.newInstance("hahaha");
}
这次反射的构造器有String类型的参数,对应的调用构造器也要传入string参数
@Test
public void test3() throws Exception {
Class class3=Class.forName("reflect.Person");
Constructor constructor3=class3.getConstructor(String.class,int.class);
Person person3=(Person) constructor3.newInstance("hahaha");
}
对应的是person类的第三个构造器,参数写成了:String.class,int.class。
@Test
public void test4() throws Exception {
Class class4=Class.forName("reflect.Person");
Constructor constructor4=class4.getDeclaredConstructor(List.class);
constructor4.setAccessible(true);//暴力反射,不论构造器是什么访问权限
Person person4=(Person) constructor4.newInstance(new ArrayList());
}
对应第四个构造器,参数类型是List,访问权限是private,在反射构造器的时候需要用getDeclaredConstructor而不是getConstructor,同时下一步需要进行暴力反射,把访问权限置为true,然后进行实例的创建。
前面我们在加载,反射了类之后都是先赋值给一个constructor构造器对象,然后调用这个constructor的newInstance对象,创建实例。其实在反射类之后,反射给的Class类也有newInstance方法,可以创建实例。
@Test
public void test5() throws Exception {
Class class5=Class.forName("reflect.Person");
Person person5=(Person)class5.newInstance();
}
但是需要注意的是,这种方法,跨过创建constructor对象,直接创建Person实例,但是只能反射无参构造函数,这个等价于我们写的test1方法。
2.2反射类的方法
考虑一般方法可能具有的形式,要么是无参,完成一个功能,要么是一个或多个参数,要么没有返回值,要么有各种类型的返回值(用来练习,越复杂越好)。我们在Person类里加入这几种可能的方法,然后在测试类里进行反射的测试。
在Person类里加入这几个方法:
public void print() {
System.out.println("this is print method");
}
//有参数,多个
public void print2(String string,int num) {
System.out.println("string and num:"+string+":"+num);
}
//参数有数组,返回值是一个Class类型的数组
public Class[] print3(String string,int[] num) {
return new Class[] {String.class};
}
//私有类型,io流
private void print4(InputStream in) {
System.out.println(in);
}
//static方法
public static void print5(int num) {
System.out.println(num);
}
然后在我们的测试类里进行反射的测试:
@Test
public void test() throws Exception {
//首先加载类
Class class1=Class.forName("reflect.Person");
//再反射,先用getMethod得到这个方法,赋值给一个Method对象,
//要用invoke调用方法运行,需要指定对某个对象以及说明对应方法的参数,因此要先new一个person对象
Person person1=new Person();
Method method1=class1.getMethod("print", null);
method1.invoke(person1, null);
}
和对构造器的反射测试一样,首先需要加载类,然后用Class类提供的getMethod方法来反射这个类里的方法,我们赋值给一个Method对象,前面对构造器的运行,我们就用.newInstance方法,就能完成实例化一个对象的功能,在类成员方法的运行,用.invoke方法,但是这个方法运行是用需要指定一个对象以及对应运行这个方法的参数,也即是说我们还需要一个Person对象,然后在.invoke运行这个指定的方法,反射就完成了。
其他四个方法过程同理:
//public void print2(String string,int num)
@Test
public void test7() throws Exception {
Class class2=Class.forName("reflect.Person");
Person person2=new Person();
//给getMethod传入参数对应的字节码
Method method2=class2.getMethod("print2", String.class,int.class);
method2.invoke(person2, "print2",100);
}
//public Class[] print3(String string,int[] num)
@Test
public void test8() throws Exception {
Class class3=Class.forName("reflect.Person");
Person person3=new Person();
//第二个参数int数组的字节码就是int[].class
Method method3=class3.getMethod("print3", String.class,int[].class);
//由于person类的本身print3方法里没有输出,为了查看结果把方法返回结果赋值给一个对象
Class[] cs=(Class[]) method3.invoke(person3, "print3",new int[] {12,23,34});//注意传参的数组对象写法
System.out.println(cs[0]);//只有一个元素
}
//private void print4(InputStream in)
@Test
public void test9() throws Exception {
Class class4=Class.forName("reflect.Person");
Person person4=new Person();
//注意是getDeclaredMethod因为是private
Method method4=class4.getDeclaredMethod("print4",InputStream.class);
method4.setAccessible(true);
//由于参数是inputStream,参数不能为空,得要写一个文件进去
method4.invoke(person4,new FileInputStream("E:\\JavaWorkspace\\mypractice10-8\\1.txt"));
}
//public static void print5(int num)
@Test
public void test10() throws Exception {
Class class5=Class.forName("reflect.Person");
Method method5=class5.getMethod("print5",int.class);
method5.invoke(null, 22);//静态方法可以不要对象,把那个参数置null,也可以有对象
}
2.3反射类的main方法
main方法需要单独说一下,我们在Person类里先加入一个mian方法:
//main方法
public static void main(String[] args) {
System.out.println("this is main method");
}
然后我们在测试类里先按照上面对普通方法进行测试的步骤进行一下测试看看:
//public static void main(String[] args)
@Test
public void test11() throws Exception{
Class class6=Class.forName("reflect.Person");
Person person6=new Person();
Method method6=class6.getMethod("main", String[].class);
method6.invoke(person6,new String[] {"hello","world"});
}
先解析类,然后新建一个Person对象,然后反射给一个Method对象,接着进行调用。
发现运行失败了。
显示的是java.lang.IllegalArgumentException: wrong number of arguments,说我们传入的参数个数错误,但是命名就是对应一个数组没有问题。为什么呢?
调用Method.invoke的时候,这个.invoke方法在jdk1.5之后接收的后面参数是一个或多个参数,method方法是长这样的:public Object invoke(Object obj, Object… args)。
而在jdk1.4之前,这个方法是这样的:public Object invoke(Object obj, Object obj[])
后面的版本向下兼容,因此即使是之后的jdk版本,这个方法在调用的时候,会把传入的数组拆开拆成两个字符串传进去,但是main方法接收的虽然是数组,只有一个参数,因此要想他执行的时候拆出来,还是一个数组,就需要在传入参数的时候在进行一次转型,我们new一个Object数组,包裹起来这个string数组:
method6.invoke(person6,new Object[] { new String[] {"hello","world"}});
然后就可以运行成功了。
或者,既然他要拆开,那么在拆之前看到的不是数组,就不会拆开了,然后传进去正好是数组就可以了,做法就是在数组前面转型成一个Object:
method6.invoke(person6,(Object)new String[] {"hello","world"});
也可以完成一样的结果
2.4反射类的字段
类里面的成员除了构造器,方法,还有字段(我们常说的属性),我们在Person类里加入几个字段,一般就是public或者private,再就是static:
public String name="john";
private int age;
private static int ID;
然后我们在测试类里反射这几个字段:
//public String name="john";
@Test
public void test12() throws Exception{
Class class7=Class.forName("reflect.Person");
Person person7=new Person();
Field field1=class7.getField("name");
String name1=(String) field1.get(person7);
System.out.println(name1);
}
首先解析类,获取到类的字节码给一个Class类,然后因为字段从属于一个类,我们要先实例化一个Person对象,然后getField方法后取到name这个字段赋值给一个Field对象,接着我们用get方法输出一下,这里会提示强转类型,因为name是字符串类型,而get方法得到的默认都是object类型。
另外,在我们不知道某个字段是什么类型的时候,反射机制可以帮助我们获得他,那就是在Field类获取到这个类的时候,我们再定义一个Class类,然后用getType方法对整个Field对象进行操作,就可以得到他是什么type:
在上面测试方法最下面再加入:
Class class1=field1.getType();
System.out.println(class1);
会看到输出:
告诉我们他是java.lang.String类型。
上面的代码其实不够严谨,因为拿到某一个字段的时候,我们应该是不知道他的类型的,那么强转成String类型之后是因为获取了类型之后再进行强转,所以先开始获取字段的时候我们应该先保存在一个Object类型的对象里而不是直接在一个String类型里。我们修改这个对第一个字段进行反射的代码如下:
//public String name="john";
@Test
public void test12() throws Exception{
Class class7=Class.forName("reflect.Person");
Person person7=new Person();
Field field1=class7.getField("name");
//获取字段
Object object=field1.get(person7);
//获取字段类型
Class class1=field1.getType();
System.out.println(class1);
if(class1.equals(String.class)) {
String name=(String)object;
System.out.println(name);
}
}
下一个字段是int age,这个注意是private,这个基本类型我们没必要像string那么麻烦,直接输出看看就行:
//private int age;
@Test
public void test13() throws Exception{
Class class8=Class.forName("reflect.Person");
Person person8=new Person();
Field field2=class8.getDeclaredField("age");
field2.setAccessible(true);
System.out.println(field2.get(person8));
}
下一个是static字段,静态字段不需要new对象:
//private static int ID;
@Test
public void test14() throws Exception{
Class class9=Class.forName("reflect.Person");
Field field3=class9.getDeclaredField("ID");
field3.setAccessible(true);
System.out.println(field3.get(null));
}