• 深入理解JAVA反射的原理和优缺点
  • 发布于 1个月前
  • 67 热度
    0 评论
在Java面试中,反射是一个常被提及的话题,但真正能深入理解和清晰阐述的人却不多。很多人对“反射”这一概念只是浅尝辄止,难以深入其底层实现原理。鉴于此,本文旨在用通俗易懂的语言系统地讲解反射,力求让每一位读者都能真正理解和掌握这项技术。希望在阅读完本文后,大家能够对反射有更深入的认识,从而在未来的开发中更加熟练地运用它。

1.什么是反射?
反射在程序运行期间动态获取类和操纵类的一种技术。java程序运行分为编译期和运行期,而反射就是发生在运行期,我们通过给定类的名字,就要可以获取类的所有属性、方法和注解信息。

我们都听说过反射机制会影响性能,所以一些诸如BeanUtils的工具类,是比不上MapStruct的,因为一个是在运行期发生的,无法让JVM来自动优化,一个是在编译期就确定了。

如果我们的程序中大量用到反射,由于反射的时候,参数需要包装成Object[]类型,后面又要转回来,这个过程很消耗性能的,而且会在过程中临时生成大量的对象,堆内存吃不消的情况下就会触发GC,频繁GC会影响整体系统的运行。

值得注意的是,反射可能会破坏单例模式,因为单例模式的原理是用私有构造方法,无法阻止反射来创建一个新的实例,解决方法也很简单,在构造方法里面判断一下实例是否以及存在,不存在的话就抛出异常即可。

2.反射的应用有哪些?
反射在日常开发中使用的地方有很多,例如以下几个:
动态代理:反射是动态代理的底层实现,即在运行时动态地创建代理对象,并拦截和增强方法调用。这常用于实现 AOP 功能,如日志记录、事务管理等。
Bean 创建:Spring/Spring Boot 项目中,在项目启动时,创建的 Bean 对象就是通过反射来实现的。

JDBC 连接:JDBC 中的 DriverManager 类通过反射加载并注册数据库驱动,这是 Java 数据库连接的标准做法。


3.反射实现
反射的关键实现方法有以下几个:
得到类:Class.forName("类名")
得到所有字段:getDeclaredFields()
得到所有方法:getDeclaredMethods()
得到构造方法:getDeclaredConstructor()
得到实例:newInstance()
调用方法:invoke()
具体使用示例如下:
// 1.反射得到对象
Class<?> clazz = Class.forName("User");
// 2.得到方法
Method method = clazz.getDeclaredMethod("publicMethod");
// 3.得到静态方法
Method staticMethod = clazz.getDeclaredMethod("staticMethod");
// 4.执行静态方法
staticMethod.invoke(clazz);
反射执行私有方法代码实现如下:
// 1.反射得到对象
Class<?> clazz = Class.forName("User");
// 2.得到私有方法
Method privateMethod = clazz.getDeclaredMethod("privateMethod");
// 3.设置私有方法可访问
privateMethod.setAccessible(true);
// 4.得到实例
Object user = clazz.getDeclaredConstructor().newInstance();
// 5.执行私有方法
privateMethod.invoke(user);
4.底层实现原理
通过深入了解反射的底层实现,我们不难发现,invoke方法在其中扮演着至关重要的角色。一旦掌握了invoke方法的执行流程,也就等于把握了反射的核心原理。那么,invoke方法究竟是如何工作的呢?

首先,当通过java.lang.reflect.Method对象调用invoke方法时,JVM会进行一系列严谨的检查。它会确认被调用的方法是否存在,并验证当前调用者是否拥有访问该方法的权限。这包括对方法的访问权限进行检查,以及确保方法签名与调用时提供的参数相匹配。

接下来,如果调用的方法是私有的或受保护的,JVM还会进行额外的安全检查。这是为了确保只有具备相应权限的调用者才能访问这些方法,从而维护Java的封装性和安全性。如果权限不足,将抛出IllegalAccessException。

在参数方面,invoke方法接收一个对象实例和一组参数,然后对这些参数进行必要的转换和适配。这包括将参数转换成对应方法签名所需要的类型,以及进行类型检查和装箱拆箱操作。这样,才能确保参数与方法的要求一致,从而顺利进行方法调用。

至于方法的实际调用过程,对于非私有方法,Java反射是通过JNI(Java Native Interface)调用到JVM内部的native方法来实现的。具体来说,会调用到类似java.lang.reflect.Method.invoke0()这样的native方法。这些native方法负责完成真正的动态方法调用,它们会利用JVM的内部机制来查找和调用相应的方法。

在方法执行过程中,如果出现任何异常,JVM会捕获这些异常,并将它们包装成InvocationTargetException抛出。这样,应用程序就可以通过捕获这个异常来获取到原始异常信息,从而进行相应的处理。

最后,如果方法正常执行完毕,invoke方法会返回方法的执行结果。如果方法的返回类型是void,则invoke方法不返回任何值。通过这种方式,invoke方法打破了编译时的绑定,实现了运行时动态调用对象的方法,为Java程序提供了极大的灵活性。

然而,需要注意的是,反射虽然强大,但也带来了运行时性能损耗和安全隐患。由于反射需要在运行时进行类型检查和动态方法调用,因此其性能通常比直接调用方法要差。此外,反射还可能破坏封装性并违反访问控制,从而给程序带来安全风险。因此,在使用反射时,我们需要谨慎权衡其利弊,确保在必要的情况下合理使用。

5.优缺点分析
反射的优点如下:
灵活性:使用反射可以在运行时动态加载类,而不需要在编译时就将类加载到程序中。这对于需要动态扩展程序功能的情况非常有用。
可扩展性:使用反射可以使程序更加灵活和可扩展,同时也可以提高程序的可维护性和可测试性。

实现更多功能:许多框架都使用反射来实现自动化配置和依赖注入等功能。例如,Spring 框架就使用反射来实现依赖注入。


反射的缺点如下:
性能问题:使用反射会带来一定的性能问题,因为反射需要在运行时动态获取类的信息,这比在编译时就获取信息要慢。

安全问题:使用反射可以访问和修改类的字段和方法,这可能会导致安全问题。因此,在使用反射时需要格外小心,确保不会对程序的安全性造成影响。


课后思考
为什么反射的执行效率比较低?动态代理的实现除了反射之外,还有没有其他的实现方法?
反射的执行效率比较低的原因主要有以下几点:
运行时类型检查和访问权限检查:使用反射时,需要在运行时进行类型检查,以确保调用的方法、访问的属性等是有效的。这涉及到了额外的运行时判断和类型转换,以及访问权限的检查和处理,这些都会带来额外的开销。

动态方法调用:通过反射调用的方法需要在运行时动态地解析方法的签名,并确定要调用的具体方法。这需要进行方法查找和动态绑定的过程,相对于直接调用方法而言更为耗时。

额外的内存空间:在运行时,每个类都会产生一个Class对象。如果使用反射,就需要额外的内存空间来存储这些Class对象,导致程序的内存占用过大,从而影响程序的性能。

关于动态代理的实现方法,除了反射之外,确实还存在其他的实现方式。例如,CGLib是一个基于ASM(一个Java字节码操作框架)的动态代理实现方式,它无需通过接口来实现,而是通过实现子类的方式来完成调用的。这种实现方式相比于JDK原生动态代理(基于反射)在某些情况下可能具有更高的性能。

用户评论