相信不少人在实际运用中都大量使用了反射的机制,无论在各种编程语言中都应用的非常广泛,无论是基础框架搭建,或者复杂的业务代码编写都经常运用反射,但是大家是否知道反射实际原理,它有什么优缺点呢?今天我就用java语言来讲一讲我们的反射吧。
反射的基本原理
(1)Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
(2)Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。
Class类继承自Object类Class类是所有类的共同的图纸。每个类有自己的对象,同时每个类也看做是一个对象,有共同的图纸Class,存放类的结构信息,能够通过相应方法取出相应的信息:类的名字、属性、方法、构造方法、父类和接口。
Class 类的实例表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有N多的实例每个类都有该Class对象。(包括基本数据类型)
Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的defineClass 方法自动构造的。不需要我们自己去处理创建,JVM已经帮我们创建好了。
反射的好处
一时反射一爽,时时反射时时爽,因为反射大量灵活运用是你走向大神标志,也是你在快速的编程,解决复杂业务逻辑必须具备的能力尤其是在实现动态创建对象和编译,体现出很大的灵活性(特别是在J2EE的开发中它的灵活性就表现的十分明显)。通过反射机制我们可以获得类的各种内容,进行了反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象。
1.能够运行时动态获取类的实例,大大提高系统的灵活性和扩展性。
2.与Java动态编译相结合,可以实现无比强大的功能
示例:
- /**
- * @description: Student
- * @Description: 学生
- * @Author: ly
- * @Date: 2022/2/13 15:47
- * @version 1.0
- */
- public class Student {
- /** 编号 */
- private int stuId;
- /** 姓名 */
- private String stuName;
- /** 性别:1-男,2-女 */
- private int stuSex;
- /** 班级 */
- private String classId;
- /** 年龄 */
- private int stuAge;
- public int getStuId() {return stuId; }
- public void setStuId(int stuId) { this.stuId = stuId; }
- public String getStuName() { return stuName; }
- public void setStuName(String stuName) { this.stuName = stuName; }
- public int getStuSex() { return stuSex; }
- public void setStuSex(int stuSex) { this.stuSex = stuSex; }
- public String getClassId() { return classId; }
- public void setClassId(String classId) { this.classId = classId; }
- public int getStuAge() { return stuAge; }
- public void setStuAge(int stuAge) { this.stuAge = stuAge; }
- }
-
-
- /**
- * @description: Student1Reflect
- * @Description: 反射类基础方法
- * @Author: ly
- * @Date: 2022/2/13 16:01
- * @version 1.0
- */
- public class Student1Reflect {
- public static void main(String[] args) {
- try {
- Class stuClass = Class.forName("Student");
- String stuClassName = stuClass.getName();
- System.out.println("名称:" + stuClassName);
- String stuClassSimpleName = stuClass.getSimpleName();
- System.out.println("简称:" + stuClassSimpleName);
- Package stuPackage = stuClass.getPackage();
- Class superClass = stuClass.getSuperclass();
- System.out.println("父类类实例:"+superClass.getName());
- Class[] interfaces = stuClass.getInterfaces();
- System.out.println("实现接口数:"+interfaces.length);
- Field[] stuFields = stuClass.getDeclaredFields();
- System.out.println("属性个数:"+stuFields.length);
- Method[] stuMethods = stuClass.getDeclaredMethods();
- System.out.println("方法个数:"+stuMethods.length);
- Constructor[] stuConstructors = stuClass.getConstructors();
- System.out.println("构造方法个数:"+stuConstructors.length);
- Constructor constructor =
- stuClass.getDeclaredConstructor(int.class,String.class,int.class,String.class,int.class);
- constructor.setAccessible(true);
- Student student = (Student) constructor.newInstance(1,"小李",1,"一班",20);
- System.out.println("修改前--编号:"+student.getStuId()+",姓名:"+student.getStuName()+",年龄:"+student.getStuAge());
- Field stuName = stuClass.getDeclaredField("stuName");
- stuName.setAccessible(true);
- stuName.set(student,"小张");
- System.out.println("修改后--编号:"+student.getStuId()+",姓名:"+student.getStuName()+",年龄:"+student.getStuAge());
-
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
-
结果:
- 名称:Student
- 简称:Student
- 父类类实例:java.lang.Object
- 实现接口数:0
- 属性个数:5
- 方法个数:10
- 构造方法个数:2
- 修改前--编号:1,姓名:小李,年龄:20
- 修改后--编号:1,姓名:小张,年龄:20
反射的坏处
在享受反射来带的便利的情况下也是要付出相应的代价的,天下没有白吃的午餐,因为反射对性能是有一定的影响的。以为使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它 满足我们的要求。这类操作总是慢于只直接执行相同的操作。尤其是在一定的数量际上尤为明显,一根稻草对马车可能不会有什么重量体现,在处理上万,十万,百万的数据时就会有明显的表现的了,这点对于初学编程或者对反射了解皮毛的同学尤为明显,往往看着没有任何问题的代码就是跑不快,为什么简单的可以使用反射来处理的逻辑要一步一步的用硬编代码来实现。
1. 每次反射都会对参数做封装和解封操作
我们可以知道,invoke方法的参数是Object[] 类型,也就是说,如果方法参数是简单类型的话,需要在此转化成Object类型,例如long ,在 javac compile 的时候用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。在数据生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。因此,在反射调用的时候,因为封装和解封,产生了额外的不必要的内存浪费,当调用次数达到一定量的时候,还会导致 GC。
2. 需要检查方法可见性
通过分析,我们会发现,反射时每次调用都必须检查方法的可见性,会造成大量的性能浪费。
3. 需要校验参数
反射时也必须检查每个实际参数与形式参数的类型匹配性(在
NativeMethodAccessorImpl.invoke0里或者生成的 Java 版 MethodAccessor.invoke里)。
4. 反射方法难以内联
Method#invoke 就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。
对比示例:
- import sun.plugin.javascript.JSObject;
- import java.lang.reflect.Constructor;
- import java.lang.reflect.Field;
- import java.lang.reflect.Method;
- import java.text.SimpleDateFormat;
- import java.util.ArrayList;
- import java.util.Date;
- import java.util.List;
-
-
- /**
- * @version 1.0
- * @description: Student1Reflect
- * @Description: 反射类基础方法
- * @Author: ly
- * @Date: 2022/2/13 16:01
- */
- public class Student1Reflect {
- /**
- * 将dto和entity之间的属性互相转换,dto中属性一般为String等基本类型,
- * 但是entity中可能有复合主键等复杂类型,需要注意同名问题
- * @param src
- * @param target
- */
- public static Object populate(Object src, Object target) {
-
-
- Method[] srcMethods = src.getClass().getMethods();
- Method[] targetMethods = target.getClass().getMethods();
- for (Method m : srcMethods) {
- String srcName = m.getName();
- if (srcName.startsWith("get")) {
- try {
- Object result = m.invoke(src);
- for (Method mm : targetMethods) {
- String targetName = mm.getName();
- if (targetName.startsWith("set")
- && targetName.substring(3, targetName.length())
- .equals(srcName.substring(3, srcName.length()))) {
- mm.invoke(target, result);
- }
- }
- } catch (Exception e) {
- }
- }
- }
- return target;
- }
-
-
- /**
- * dto集合和实体类集合间的互相属性映射
- * @param src
- * @param target
- * @param targetClass
- * @return
- */
- @SuppressWarnings("unchecked")
- public static <S, T> List<T> populateList(List<S> src, List<T> target, Class<?> targetClass) {
- for (int i = 0; i < src.size(); i++) {
- try {
- Object object = targetClass.newInstance();
- target.add((T) object);
- populate(src.get(i), object);
- } catch (Exception e) {
- continue;// 某个方法反射异常
- }
- }
-
-
- return target;
-
-
- }
-
-
- public static void main(String[] args) {
- try {
- List<Student> studentList1 = new ArrayList<>();
- List<Student> studentList2 = new ArrayList<>();
- //直接赋值
- Date beginTime0= new Date();
- for (int i = 1; i < 1001; i++) {
- Student s = new Student(i, "姓名"+i, 1, "班级1", i);
- studentList1.add(s);
- }
- Date endTime0= new Date();
- //反射赋值
- Date beginTime1= new Date();
- populateList(studentList1,studentList2,Student.class);
- Date endTime1= new Date();
- //直接赋值打印参数
- String beginTimeStr0 = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(beginTime0);
- String endTimeStr0 = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(endTime0);
- long consuming1Time0 = endTime0.getTime() - beginTime0.getTime();
- StringBuilder sb0=new StringBuilder();
- sb0.append("直接赋值总数量:"+studentList1.size());
- sb0.append(" 开始时间:"+beginTimeStr0);
- sb0.append(" 结束时间:"+endTimeStr0);
- sb0.append(" 耗时:"+consuming1Time0);
- sb0.append(" \r\n");
- System.out.println(sb0.toString());
- //反射赋值打印参数
- String beginTimeStr1 = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(beginTime1);
- String endTimeStr1 = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(endTime1);
- long consuming1Time1 = endTime1.getTime() - beginTime1.getTime();
- StringBuilder sb1=new StringBuilder();
- sb1.append("反射赋值总数量:"+studentList1.size());
- sb1.append(" 开始时间:"+beginTimeStr1);
- sb1.append(" 结束时间:"+endTimeStr1);
- sb1.append(" 耗时:"+consuming1Time1);
- sb1.append(" \r\n");
- System.out.println(sb1.toString());
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
-
数据对比(本机I5处理器,8G内存,实体只有5个属性值)
直接,反射赋值对比 | ||||
序号 | 数量 | 直接赋值(毫秒) | 反射赋值(毫秒) | 时间差 |
1 | 10 | 1 | 1 | 0 |
2 | 100 | 1 | 9 | 8 |
3 | 1000 | 2 | 18 | 16 |
4 | 10000 | 4 | 88 | 84 |
5 | 100000 | 30 | 354 | 324 |
6 | 1000000 | 108 | 2229 | 2121 |
运行模式说明:
针对所有的list赋值都会有这样一个处理逻辑图(无限循环无限套娃模式),但是针对非反射模式都会有这样要给逻辑图就(无限示例代码处理)。
结论:
1.直接赋值比反射赋值执行速度快得多,在数量级越大,效果越明显,单个实体属性值越多,消耗越是明显;
2.直接赋值需要对一个参数进行set,工作量比较大,反射赋值不需要对每一个参数进行设置,通用性非常强,适用于基础方法,数量比较小的场景;
3.测试数据只针对于本示例,list转换也可以对反射进行优化,但是反射比直接使用耗时是肯定的,消耗也是肯定的;
如何正确的运用反射
我们对反射一定要注意其运用的场景,他可以动态的解析类,方法,变量等;但是要注意批量处理时无用的重复调用,基本上就能正确的使用反射。
1.反射多用于基础方法设置,数量级小的情况,复用性非常强;
2.对重复使用量大情况不适合反射,比如list之间的转化,文件之间转换等;
3.反射就是用于原子性越小越适合,原子性越大越不适合反射;