JVM在加载完类之后,会在方法区产生一个对应的Class类型的对象,这个对象包含了完整类的结构信息。这个对象就像一面镜子,可以反映类的内部结构,我们形象地称这种机制为「反射」。反射的使用场景有很多,比如 Spring IOC 便利用了反射机制,接下我们来聊一下反射。

Reflection

反射(Reflection),指在程序运行期间可以动态获取一个类的所有信息,并且能够直接操作任意对象的内部属性以及方法。

反射是「动态语言」的关键,,可以使得编程更加灵活。

1、概述

JVM的「类加载子系统」可以从文件系统或者网络中加载.class文件。加载之后的.class文件会在类加载子系统执行:加载、链接、初始化

image-20201120221030003

在加载阶段,JVM通过类的全限定类名获取定义此类的二进制字节流,将字节流代表的静态存储结构转换为方法区的运行时数据结构,并在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

通过内存中的这个java.lang.Class,可以映射类的所有信息,因此我们形象地称这种机制为:「反射」

2、Class

2.1 Class获取的方式

从上面可以知道,与类关联的Class是在加载过程生成的,可以反映类的结构信息。那么我们该然后获取到这个Class呢?主要有以下几种方式。

  • 通过类的静态属性class,这种方式比较安全,性能较高
  • 通过实例的getClass方法,getClass是Object的弍native方法
  • 通过全限定类名,JDBC的驱动加载使用过这种方式

获取Class的几种方式

1
2
3
4
5
6
7
8
9
10
public class ReflectionDemo {
public static void main(String[] args) throws ClassNotFoundException {
// 方式1:通过类的class属性获取
Class<ReflectionDemo> clazz1 = ReflectionDemo.class;
// 方式2:通过类的实例,调用getClass方法
Class<? extends ReflectionDemo> clazz2 = new ReflectionDemo().getClass();
// 方式3:通过类的全限定类名
Class<? extends Class> clazz3 = Class.forName("top.tobing.review.ReflectionDemo").getClass();
}
}

2.2 Class的特征

Class 中可以获取某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。值得注意的是,Class具有以下特征。

  • Class本身也是一个类
  • Class 对象只能由系统建立对象
  • 一个加载的类在 JVM 中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的一个.class文件
  • 每个类的实例都会记得自己是由哪个 Class 实例所生成
  • 通过Class可以完整地得到一个类中的所有被加载的结构
  • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的 Class对象

Class中常用API

方法名称 说明
static Class forName(String name) 返回指定类名 name 的 Class 对象
Object newInstance() 调用缺省构造函数,返回该Class对象的一个实例
getName() 返回此Class对象所表示的实体(类、接口、数组类、基本类型 或void)名称
Class getSuperClass() 返回当前Class对象的父类的Class对象
Class [] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Class getSuperclass() 返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors() 返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields() 返回Field对象的一个数组
Method getMethod(String name,Class … paramTypes) 返回一个Method对象,此对象的形参类型为paramType

利用反射获取一个类的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ReflectionDemo02 {
public static void main(String[] args) {
Class<String> clazz = String.class;
StringBuilder res = new StringBuilder();
for (Method method : clazz.getMethods()) {
res.append(method.getReturnType().getSimpleName());
res.append(" ");
res.append(method.getName());
res.append("(");
Parameter[] parameters = method.getParameters();
if (parameters != null && parameters.length != 0) {
for (int i = 0; i < parameters.length; i++) {
res.append(parameters[i].getType().getSimpleName());
res.append(" ");
res.append(parameters[i].getName());
if (i != parameters.length - 1) {
res.append(", ");
}
}
}
res.append(")\n");
}
System.out.println(res.toString());
}
}

3、反射的应用

我们常用的Spring框架底层大量使用了反射技术。

3.1 Spring IOC

IOC,Inversion Of Control,控制反转,即将对象生命周期和对象间关系等控制权从程序员手上交到Spring IOC容器中。IOC并不是一种具体的技术,而是一种设计思想。

为什么使用Spring IOC

传统的Java SE 编程中,类的数量不多,类之间的关系不复杂,可以通过new在对象内部创建需要依赖的对象,这是一种主动的方式来创建对象;

然而在Java EE编程中,类的数量开始变多,对象之间相互依赖开始变的复杂,Controller层的一个实例可能依赖了若干个Service层的实例,Service层实例可能又依赖了若干个Dao层实例,不同Service、Controller中可能依赖同一个对象;

这时,如果再通过手动的方式为每个类创建依赖的对象,则对象之间存在较高的耦合度,维护起来并不方便。于是Spring 通过IOC的方式,将对象生命周期以及依赖关系管理的控制权从编码人员手上移到了Spring手上。Spring可以提前创建好需要使用的对象,并将这些对象放到IOC容器中进行管理,当需要某个对象时,只需要通过@Autowried等方式来将对象注入。这样编码人员便不用关心类的创建以及依赖管理等细节,可以将精力放到实际的逻辑中,从而提高开发效率。

如何实现 IOC

那么Spring是如何通过一个@Autowried注解就可以实现依赖对象的注入呢?答案尽在反射技术。

SpringIOC

Web容器在启动时会启动Spring容器,Spring容器在启动时会通过xml、或者注解扫描的方式来创建出各种Bean,这种在运行期间创建对象的方式正是反射。

详细将会在「再谈Spring」中见。

4、反射的优缺点

4.1 优点

  • 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
  • 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
  • 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。

4.2 缺点

尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。

  • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
  • 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
  • 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

5、总结

在JVM会在类加载阶段生成与类唯一的匹配的Class对象放到内存中,通过这个Class对象可以反映类的内部结构,如方法、变量、构造器等信息,通过Class对象的还可以动态地创建该类的实例,我们形象地将这种机制称为「反射」。如果想要通过「反射」来动态创建类的实例,就要先获取该类的Class对象,主要有三种方式获取:

  • 类名.class
  • 实例.getClass()
  • Class.forName(“全限定类名”)

反射的应用场景比较广泛,在一些框架中被广泛适应,如Spring中实现控制反转的时就是利用反射来创建对象,并将对象放到容器中管理。

6、参考

评论