深入浅出!全面剖析Java反射 Reflection

时间:2021-6-12 作者:qvyue

前言

虚拟机的语言无关性

我们知道,虚拟机规范了Class文件(是一种二进制文件)的标准结构,Class文件中每个字节代表的含义是确定的,譬如每个类文件的头4个字节称为魔数,任一符合标准结构的Class文件都可以被虚拟机识别并加载。但虚拟机并不关心Class文件的来源,它有可能是由java程序编译生成的,也有可能是Scala程序编译生成的,即所谓的语言无关性。通俗的讲,只要把符合规范的Class文件给虚拟机,虚拟机就可以按照规定步骤完成加载、链接、初始化等操作。

对于java语言来说,java程序的业务源代码经过javac编译后,会为每个业务类生成一份对应的XXX.class类文件,每一个类文件以二进制的形式存储,其中包含了描述对应业务类的全部信息。这个类文件符合虚拟机的规范结构,因此可以被虚拟机识别并加载。需要指出的是,利用每个业务类的类文件,虚拟机可以在内存中生成一个对应的java.lang.Class类的实例。

虚拟机的平台无关性是向下无关,而语言无关性是向上无关。

动态加载Class文件

动态加载Class文件可以分为三个部分解释:即动态、加载、Class文件。Class文件是指源程序编译之后生成的二进制目标文件,以“.class”为扩展名,注意根据上下文区分Class文件同java.lang.Class类型。需要说明的是,此处的Class文件指普通类的Class文件,譬如Person类的Person.class文件。而java运行所需要的基本类采用预先加载的方式全部加载到内存中,其内容不再此处讨论。

接下来解释动态和加载。

加载

java程序生命周期可分为编译和运行两大步骤,首先完成程序编译,其后运行程序。

在编译阶段,源文件由编译器编译成字节码。我们编写众多java业务类,通过编译之后便会在磁盘中生成一个个class后缀的类文件。注意此时这个类的类文件尚未被虚拟机加载,只是以二进制文件形式存在在磁盘中。

在运行阶段,虚拟机解释执行字节码。该阶段首先要完成类加载的过程。类加载机制是指虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。类加载的全过程包括加载、验证、准备、解析和初始化这5个阶段,要注意区分的是,“加载”是“类加载”过程中的一个阶段。加载阶段,虚拟机完成三件事情

  1. 通过一个类的全限定名来获取定义此类的二进制字节流,即按名找类文件
  2. 将字节流所代表的静态存储结构转化为内存中方法区的运行时数据结构,即类文件进内存。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口,即文件变对象。

上面的内容描述了虚拟机加载Class文件在java程序生命周期中所处的位置,即运行阶段的第一个阶段(类加载阶段)的第一个阶段(加载阶段)。

特别强调的是,一个普通类的Class文件只会被加载一次,即第一次加载完成之后,虚拟机可以在任何时候找到并使用这个文件。

动态

那么所谓动态是指,虚拟机并不是一次性把全部Class文件都加载到内存中,而是在第一次需要用到时才加载。举个例子,假设我们编写包含A、B、C、D、E五个类的程序,其中类A是程序入口且调用了类B,类B调用了类C,类C在某些情况下调用了类D,类E跟其他类没有调用关系。以下是执行该程序的步骤

  • 编译程序后,磁盘中生成了五个类的二进制文件。
  • 运行程序,那么虚拟机会先将A.class文件加载进内存,同时利用这个二进制文件生一个java.lang.Class类的实例,该实例用于描述类A的全部信息。
  • 在执行类A期间,发现其调用了类B。那么虚拟机将B.class文件加载进内存,并生成对应的java.lang.Class类的实例。
  • 同样地,执行B期间调用了类C。虚拟机将C.class文件加载进内存,并生成对应的java.lang.Class类的实例。
  • 假设初始情况下,类C不调用类D。那么虚拟机并不会将D.class文件加载进内存。以上步骤发生在程序运行期间,是程序根据类间调用关系完成首次的类文件的动态加载,即需要哪个类就加载其对应的Class文件。
  • 随着程序的运行,类C需要调用类D,此时虚拟机会从磁盘中将D.class加载进内存,并生成对应的java.lang.Class类的实例,然后供类C调用。此处也体现了动态加载的思想,即在程序运行过程中,虚拟机仍可根据需要加载Class文件。
  • 在整个程序运行过程中,由于类E不同其他类交互,不存在被调用的机会,因此E.class文件会一直呆在磁盘中,不会被虚拟机加载到内存中。

定义

反射(Reflection) 是 Java 在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。通俗地讲,有了java提供的反射机制,我们可在运行时拿到一个对象所属类的全部信息。

从定义可知,反射是java提供给开发者操作业务类的一种能力,而这种能力是基于java.lang.Class类型。java.lang.Class类提供了众多方法,譬如getField()、getMethod()等等,正是这些方法赋予了开发者反射定义中所描述的能力。

定义中给出了反射发挥作用所处的时间段是在程序运行时,这是因为程序开始运行之后,虚拟机会按需把普通类的类文件动态加载进内存,然后虚拟机就可以拿到这些普通类对应的类文件进而实现反射所描述的功能。这个“按需”的本质含义是类加载的时机,可参考《深入理解java虚拟机》7.2小节。

java.lang.Class类型

java.lang.Class类是一种类类型,java.lang.Class实例用于存储对应的普通类的全部的描述信息。每一个java.lang.Class的实例都对应一个普通类,且是一对一的关系,跟普通类的实例数量无关。

创建Class类型实例

java.lang.Class类的构造函数是私有的,不能使用new创建java.lang.Class实例,只能由虚拟机根据加载的class文件创建。正如“动态加载Class文件”小节所描述,虚拟机会根据需要,动态加载所需类的Class文件到内存中。这个Class文件虽然是二进制文件,但是它符合虚拟机规范,因此虚拟机可以识别它所代表的含义。虚拟机拿到Class文件之后,便根据这个文件在内存中创建一个用于描述普通类的java.lang.Class类型实例。

获取Class类型实例

虽然无法创建java.lang.Class实例,但是虚拟机提供了三种方法获取java.lang.Class对象。

  1. 实例.getClass()。由于Object类是所有java类的父类,且Object提供getClass方法,因此任何类都可以使用getClass方法获取对应的Class实例。
Person p = new Person(); 
Class clazz = p.getClass();
  1. 类名.class。使用类的属性。
Class clazz = Person.class //Class实例命名用clazz
  1. Class.forName(“普通类的全限定路径”)。这种方法多用于利用反射机制创建普通类实例。
Class clazz = Class.forName("com.guo.Person")

反射用这种方式是因为是在不清楚普通类的信息的前提下,根据类名创建Class实例,然后进行创建普通类实例等操作。注意此时只是知道普通类的路径名信息,该普通类的Class文件可能尚未加载到内存中,即在编译阶段尚未确定是否调用该类。而前两种方式都是先调用普通类,然后获取Class实例,这要求提前获取普通类的信息,与反射中事前不清楚类信息的场景不符合。

Class类常用方法

Class类提供许多方法,能够全面控制Class实例对应的普通类。本文从实现功能的角度,介绍一些Class类常用的方法。

以下方法都是由Class实例调用,操作结果对应Class实例对应的普通类。

获取普通类的构造方法

根据Class类实例访问其对应的普通类的构造方法,涉及以下方法

Constructor getConstructor(Class>… parameterTypes) 获取指定的public的构造器

  • Class>… parameterType表示构造器的参数类型
  • 参数的三个点表示可变参数列表,即该方法可以传入零到多个Class类型参数
  • 譬如当构造器的参数是String类型时,此处应该传入 String.class

Constructor[] getConstructors() 获取全部public的构造器

Constructor getDeclaredConstrnctor(Class>… parameterTypes) 获取某个构造器,不分公有、私有

  • (Class>… parameterType表示构造器的参数类型
  • 譬如当构造器的参数是String类型时,此处应该传入 String.class

Constructor[] getDeclaredConstructors() 获取全部构造器,不分公有、私有

tips:

  • 构造器总是作用在当前类,和父类无关,因此不存在多态问题。
  • 加”Declared“表示获取当前类的全部构造器,不分公私。而不加“Declared”表示当前类的公有构造器。
  • 加“s”表示获取全部,返回值是个数组。

以上方法的返回值类型是Constructor类,位于java.lang.reflect中。该类的构造器为私有,只能通过Class实例获取。用于描述普通类的构造器全部信息,包括修饰符、构造器名称、构造器参数。通过Constructor类提供的getName()、getModifiers()等方法,我们可以获取一个类的构造器的名称、参数等信息。

Constructor类提供了一个public T newInstance(Object … initargs)方法,利用该方法可以根据指定的构造方法创建对应的普通类的一个实例,但创建实例时newInstance方法中的参数列表必须与获取Constructor实例的getConstructor方法中的参数列表一致。与Constructor类不同,Class类也提供一个newInstance()方法用于创建对应普通类的实例,但这个方法要求普通类必须有无参构造方法,实质上Class类的newInstance()方法底层也是调用Constructor类的newInstance()方法。类似如下代码

Class clazz = Class.forName("com.guo.Person")
     //使用Class实例创建实例,只能调用无参构造方法,因此要求普通类必须有无参构造方法。
    Person p1 = (Person) clazz.newInstance();   
    //获取指定构造函数的类实例
    Constructor con = clazz.getConstructor(String.class, int.class);
    Person p2 = (Person) con.newInstance("lisi", 30);

获取普通类的字段

根据Class类实例访问其对应的普通类的字段信息,涉及以下方法

  • Field getField(name):根据字段名获取某个public的Field实例(包括父类)
  • Field[] getFields():获取全部public的Field实例(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的的某个Field实例(不包括父类)
  • Field[] getDeclaredFields():获取当前类的全部Field实例

tips:

  • 加”Declared“表示获取当前类的属性,包括公有、私有。
  • 不加“Declared”表示获取当前类和父类的公有属性。
  • 加“s”表示获取全部,返回值是个数组。

以上方法的返回值类型是Field类,位于java.lang.reflect中。该类的构造器为私有,只能通过Class实例获取。用于描述类或接口的字段的全部信息,包括修饰符、字段类型、字段名称。通过Field类提供的getName()、getType()、set(Object,Object)、setAccessible(Boolean)等方法,我们可以获取、修改一个类的字段的名称、值或访问权限。

获取普通类的方法

根据Class类实例访问其对应的普通类的方法信息,涉及以下方法

Method getMethod(String name,Class>… parameterTypes) 获取指定public的 Method实例(包括父类)。

  • 参数 name 为要获取的方法名
  • parameterTypes 为指定方法的参数的 Class,由于可能存在多个同名的重载方法,所以只有提供正确的 parameterTypes 才能准确的获取到指定的 Method
  • 参数的三个点表示可变参数列表,即该方法可以传入零到多个Class类型参数

Method[] getMehods() 获取全部public的Method实例(包括父类)

Method getDeclaredMethod(String name,Class>…parameterTypes) 获取当前类指定的Method实例(不包括父类)

  • 参数 name 为要获取的方法名
  • 参数的三个点表示可变参数列表,即该方法可以传入零到多个Class类型参数
  • parameterTypes 为指定方法的参数的 Class,由于可能存在多个同名的重载方法,所以只有提供正确的 parameterTypes 才能准确的获取到指定的 Method

Method[] getDeclaredMethods() 获取当前类全部的Method实例(不包括父类)

tips:

  • 加”Declared“表示获取当前类指定方法,包括公有、私有。
  • 不加“Declared”表示获取当前类和父类的公有属性。
  • 加“s”表示获取全部,返回值是个数组。

以上方法的返回值类型是Method类,位于java.lang.reflect中。该类的构造器为私有,只能通过Class实例获取。用于描述一个方法签名的全部信息,包括方法修饰符、方法返回值类型、方法名称、方法参数。通过Method类提供的getName()、getReturnType()、getParameterTypes()、setAccessible(Boolean)等方法,我们可以获取、调用、修改一个类的方法的名称、返回值或访问权限。

特点

反射的优点

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

反射的缺点

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

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

作用

反射的定义已经给出了反射的作用,它其实代表一种机制,或者说能力。准确的说,我们可以用反射这种机制完成什么样的功能需求,上一小节中获取普通类的构造方法、获取普通类的字段、获取普通类的方法就是反射所描述能力的体现。那么常用的场景有

  • 根据普通类的全限定名创建实例
    由于普通类的全限定名是一个字符串,因此这个字符串可以在运行时动态传输给程序,字符串来源可以是网络也可以是配置文件。然后使用Class.forName(“普通类的全限定路径”)获取普通类对应的Class实例(如果内存中没有Class实例,会触发动态加载Class文件到内存),再使用Class类或Constructor类的newInstance()方法创建一个普通类实例。这个创建实例的过程不使用new方式,即不必我们在编辑代码阶段硬编码写进去,而是根据程序实时运行状况的需要确定创建实例,达到了解耦。
  • 用Method.(Object obj, Object… args)执行方法
    反射调用方法是动态代理中的核心,具体参看动态代理
  • IDE的“.”智能提示
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。