反射技术是java的核心技术之一,虽然我们日常开发中,基本上可能用的并不多,但是它同样也是必学的。因为很多框架的设计其实都是有利用反射机制的,这意味着反射是我们向前迈进的一个重要技术。
一、反射是什么?
首先我们看一段普通的调用代码
先创建一个平平无奇的Person类,有一个平平无奇的work方法
package com.lzh.reflect;
public class Person {
public void work(String content) {
System.out.println(content);
}
}
再创建个Test类进行调用
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.work("编码中");
}
}
结果不用贴了,只是将字符串“编码中”,输出到了控制台。
然后现在我们利用反射机制,做相同的调用操作:
public class Test {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, ClassNotFoundException {
Class clz = Class.forName("com.lzh.reflect.Person");
Method method = clz.getMethod("work",String.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object,"编码中");
}
}
??
别看这一坨代码比较臃肿复杂,但是它的执行效果和前面的普通调用其实是一样的。只是它是通过反射去调用的,最明显的区别就是,反射在调用同一个类的同一个方法时,并没有指定类。听起来有点绕,但是如果你的Person和Test两个文件在不同的包下,就可以看出来,普通的调用必须import com.lzh.reflect.Person;
,导入Person类。但如果是反射,就不需要导入具体的包,而是通过运行时分析参数里的类路径字符串判断出是哪个类。
站在编码者的角度,即便用了反射,我们也知道是用了哪个类,调用了哪个方法,但是对于jvm来说,它们不运行到这一步是不会知道的。在编译时,类的路径名、方法名对它来说只是个字符串参数而已。
通过反射我们甚至可以通过修改配置或传特定的参数,无需重启服务器,就能更改服务器内部的调用逻辑,所以这点也是一个安全隐患,在使用反射时一定要小心使用外部输入的参数。
总结:普通调用在编译后,就知道了要加载哪个类,调用这个类的哪个方法。但是反射是在运行时才知道要操作的类是什么,并且在运行时获取类的某个构造方法构建对象,然后调用类的某个方法。
二、怎么使用反射
1.获取Class
首先我们需要通过反射获取一个类的对象
①Class.forName(类的全路径) 静态方法
第一种方式,就是我们前面例子用的方法了。使用Class.forName(类的全路径) 指定类,这种方式我们需要知道类的全路径名,如果路径有误加载不到类,会抛出ClassNotFoundException异常。
Class clz = Class.forName("com.lzh.reflect.Person");
②.class方法
这种方法是需要导入类的,指定了类名,看起来好像有点脱裤子放屁没啥用,因为反射不就是因为不能确定类才用反射吗。但是如果我们重点关注的是反射调用哪个方法或哪个构造函数,而Class其实是已经确定的,所以并不需要在Class上费时间时,是可以用到这种方式的。
Class clz = Person.class;
③对象的getClass()方法
这种方式就更简单了,直接通过对象获取类,比方式②还要具体,这次连构造方法都无需关注了。同样,可能在我们只关注method的时候可以用上。
Person person = new Person();
Class clz1 = person.getClass();
④基本类型的Type属性
如果是基本类,我们可以通过对应包装类的TYPE属性,获取它基本类的Class,每个基本类型都有这个TYPE属性。
Class byteClass = Byte.TYPE;
Class shortClass = Short.TYPE;
Class integerClass = Integer.TYPE;
Class longClass = Long.TYPE;
Class charClass = Character.TYPE;
Class floatClass = Float.TYPE;
Class doubleClass = Double.TYPE;
Class boolClass = Boolean.TYPE;
或简便写成
Class byteClass = byte.class;
Class shortClass = short.class;
Class integerClass = int.class;
Class longClass = long.class;
Class charClass = char.class;
Class floatClass = float.class;
Class doubleClass = double.class;
Class boolClass = boolean.class;
也可以简便的写成int.class、byte.class这样,效果是一样的,通过反编译可以看到int.class最后实际上也是Integer.TYPE。
但是注意,这并不等于Integer.class,基本类型的Class类型和包装类的Class类型是两码事,它们并不是一个东西。
⑤类Class
我们也可以在获取了子类Class对象后,通过getSuperclass()获取它的父类Class。
如下,Student的父类是Person:
Student student = new Student();
Class slz = student.getClass();
Class superclass = slz.getSuperclass();
2.获取对象
获取了Class后,我们自然需要通过某个构造方法获取这个类的对象
①通过Class对象的newInstance()方法
直接通过类获取实例,但是原理是调用无参构造方法,所以如果这个类没有无参构造方法,会抛出InstantiationException异常。
Class clz = Person.class;
Person person = (Person) clz.newInstance();
②通过Constructor 对象的 newInstance() 方法
如果我们想调用指定的构造方法,就得用这种方式了。先给Person类随便加一个有参的构造方法:
public class Person {
//有参构造方法
public Person(String name) {
System.out.println(name);
}
public void work(String content) {
System.out.println(content);
}
}
然后通过clz.getConstructor(String.class) 获取指定参数的构造器(如果是获取无参构造就不传参数),这里参数也是类的Class,最后通过构造器的newInstance()方法调用构造方法,同时这里需要传入前面声明的参数:
Class clz = Person.class;
Constructor constructor = clz.getConstructor(String.class);
Person person = (Person) constructor.newInstance("123");
注意这里的newInstance()方法是构造器Constructor对象中的方法,和前面的Class中的不是一个方法,所以也会抛出更多种类的异常。
③私有构造方法
如果这个构造方法是私有的呢,给工具人Person类加一个私有的构造方法:
private Person(Integer age) {
System.out.println(age);
}
如果还是用前面的getConstructor(Integer.class);就会发现抛出了NoSuchMethodException异常,找不到这个私有的构造方法,这是因为普通的getConstructor只能获取到public级别的构造方法。
这时候我们只需要将getConstructor()替换成getDeclaredConstructor()方法即可,其实就是加了个关键字Declared,它可以读取到所有已声明的构造方法,这样即便是私有的也可以找到。另外还需要额外设置一下setAccessible为true,关闭反射对象的访问检查,不然也是无法访问私有构造方法的。
Class clz = Person.class;
Constructor constructor = clz.getDeclaredConstructor(Integer.class);
constructor.setAccessible(true);
Person person = (Person) constructor.newInstance(18);
3.获取成员变量
首先还是掏出工具人Person类,给它上两个变量,其中一个设为了私有。同时为了看结果,我重新生成了toString方法。
public String name;
private Integer age;
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
①通过getField()方法获取成员变量
通过getField()获取指定的成员变量,可以通过getName获取成员变量的名字,getType获取成员变量的类型
Class clz = Person.class;
Field field = clz.getField("name");
System.out.println(fields.getName());//获取名字
System.out.println(fields.getType());//获取类型
如果要获取值,则需要传入一个对象才行,然后直接调用get方法:
//初始化一个Person对象,并赋给成员变量一个值
Person person = new Person();
person.name = "张三";
//通过反射取出这个成员变量的值
Class clz = person.getClass();
Field field = clz.getField("name");//指定field是name变量
System.out.println(field.get(person));//获取传入对象person的name成员变量值
②通过field.set()为对象设置成员变量值
和get的使用方法一样,set也要传入一个Person对象,不然它怎么知道是给哪个对象设置值呢?这里我们试着用反射构造一个对象,再反射set变量属性后反射get出来:
Class clz = Person.class; //声明Class
Constructor clzConstructors = clz.getConstructor(); //获取默认的无参构造器
Object person = clz.newInstance();//初始化person对象
//获取成员属性,并set值进去
Field field = clz.getField("name");
field.set(person, "马德");
//输出对象的属性
System.out.println(field.get(person));
有人可能觉得你用了同一个Field对象set完了再get,当然get的到啊。实际上就算你在get的步骤前重新初始化一个Field对象去get,结果也是一样的。field并不会绑定在某一个对象上。
③私有成员变量
和构造器一样,只需要加上Declared,用getDeclaredField() ,就可以获取私有级别的成员变量。记住,没有加Declared的方法都是只能访问public级别的,不管是构造器还是成员变量还是方法。
并且也都需要setAccessible(true);关闭访问级别的检查:
Field field = clz.getDeclaredField("age");
field.setAccessible(true);
field.set(person, 18);
System.out.println(field.get(person));
4.获取和调用方法
最后终于到核心了,利用反射调用方法
①通过getMethod()获取方法,invoke调用方法
这里我偷了个懒,直接把最前面文章开头的例子拿过来了,方法还是普通的work方法
public void work(String content) {
System.out.println(content);
}
这次,我们应该能看懂这个调用了
Class clz = Class.forName("com.lzh.reflect.Person");
Method method = clz.getMethod("work",String.class); //获取work(String)方法
Constructor constructor = clz.getConstructor(); //获取构造器
Object object = constructor.newInstance(); //获取对象实例
method.invoke(object,"编码中"); //调用object对象的work方法,并传入参数
首先通过clz.getMethod("work",String.class); 获取了work方法,第二个包括以后的参数代表work方法的参数,有几个就要填几个,不然如果work方法有多个重载,反射无法找到具体的方法。
最后调用时是用method.invoke(object,"编码中"); 方法,第一个参数是要执行哪个对象的work方法,和field对象的getset方法类似,第二个包括以后的参数就是work方法所需的参数了,数量要对上,不然会抛出IllegalArgumentException异常。
②调用私有方法
估计大家都猜到怎么调用私有了,getDeclaredMethod() 可以访问到所有方法,包括私有方法。记得如果没加Declared,就只能查到public级别的。所以一般我们用getDeclaredMethod就行,稳妥。
并且要记得method.setAccessible(true)。
然后我们整个最完整的反射,没有import导入Person类,完整的调用一次方法
Person类:
public class Person {
public String name;
private Integer age; //私有属性年龄
public Person(String name) { //公共有参构造方法
this.name = name;
}
private String play(String game) { //私有有参方法,且用到了成员变量
return this.name+"("+this.age+") "+"play ♂ " + game;
}
}
调用的测试方法:
//通过反射获取Class
Class clz = Class.forName("com.lzh.reflect.Person");
//获取有参构造器,那个初始化同时会设置name的构造器
Constructor constructor = clz.getDeclaredConstructor(String.class);
//获取实例,并初始化了name属性
Object person = constructor.newInstance("比利");
//手动获取age成员属性,并设置值
Field field = clz.getDeclaredField("age");
field.setAccessible(true);
field.set(person, 28);
//调用方法
Method method = clz.getDeclaredMethod("play", String.class);
method.setAccessible(true);
Object invoke = method.invoke(person,"摔跤");
System.out.println(invoke);
这次放一次输出结果:
比利(28) play ♂ 摔跤
*5.一些其他的方法
再简单说明和反射相关的几个其他方法,非必读,可选择性跳过。
①获取构造器、方法、变量加s
Class对象中,我们已经知道了有getConstructor()和getDeclaredConstructor()获取构造器,getMethod()和getDeclaredMethod()是获取方法,getField()和getDeclaredField()是获取属性。
但是如果在这些方法名的尾部加个s,就代表获取全部的构造器/方法/属性,
返回类型是:Constructor或Method或Field类型的数组
Constructor[] clzConstructors = clz.getConstructors();
Constructor[] declaredConstructors = clz.getDeclaredConstructors();
Field[] fields = clz.getFields();
Method[] methods = clz.getMethods();
②getModifiers()
Constructor、Method、Field对象都有方法getModifiers(),返回的是一个整数值,它代表当前的方法或属性前面的修饰符相加得到的值:无任何修饰符是0 , public是1 ,private是2 ,protected是4,static是8 ,final是16。例如一个方法前面的修饰符是public static final ,那么它的值就是相加得到25。
返回类型是:int
这个方法虽然只返回一个数字,但是通过计算是一定能准确的得到修饰符的,因为都是2的倍数,所以每个返回的数字只会有计算出一种结果。
而且我们也没必要计算,反射有提供Modifier.toString() 方法,帮我们把这个数字转换为修饰符:
System.out.println("修饰符: "+Modifier.toString(method.getModifiers()));
③getParameterTypes()
Constructor、Method对象的方法getParameterTypes(),返回当前方法的参数列表,是数组格式的Class列表。
返回类型是:Class<?>[]
System.out.println(Arrays.toString(work.getParameterTypes()));
三、总结
至此,只要认真的看完并自己尝试后,我们对于反射就算没入门,也至少会用了。
我也是工作了一年后才开始学习的反射,因为工作中没怎么用过,呃,应该说就没用过。
但是当我们查看一些牛逼框架代码的源码时,就会发现里面很多地方有用到反射,就拿我们耳熟能详的Spring来说,它的核心IOC和AOP功能也是依靠了反射的。所以就算只是为了看源码,为了应付面试官,我们也应当学会反射,学了反射大家都是人上人。
参考资料:
大白话说Java反射:入门、使用、原理
https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html
Java反射技术详解
网友评论