前言
反射是Java众多框架的基石,开发中我们或许很少真的去写,但需要有基本的了解。
Java数据类型
我们都知道,Java数据类型包括基本类型和引用类型,引用类型又包括数组(array)、类(class)和接口(interface)。事实上,我们可以从创建时是否需要使用new
关键字来判断这一点。
在Java中,所有数据类型都实现了Type
接口,如果你不相信,请打开你的编辑器输入以下内容:
1 | // author: SilenceZheng66 |
Type存在于java.lang.reflect下,是所有Java类型的通用超级接口。其中包括原始类型(raw types)、参数化类型(parameterized types)、数组类型(array types)、类型变量(type variables)和基本类型(primitive types)。
Wait,这好像有点神奇,数据类型名.class
到底是什么?我们现在唯一可以确定的是,它是一个具有getTypeName()
方法的对象(或称为实例,这是为了与“类”的概念进行区分,具体与抽象的区别)。让我们来试试:
1 | // author: SilenceZheng66 |
OK,这个对象还拥有toString()
方法,似乎我们可以通过它获取主体的数据类型,举例来说,对数据类型 String[]
,其结果为class [Ljava.lang.String;
,前面的 class
表明这是一个类,后面的 [Ljava.lang.String
则是 String类型一维数组
的类名。
你可能会感到奇怪,为什么数组会是一个类?数组和类明明是两种不同的数据类型啊。我们可以从 Object
中找到答案。
Class Object is the root of the class hierarchy. Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.
而 Object
是如何定义的呢?public class Object
!因此,Java数组也是一个 class
,但它的实现是存在于Java的语言实现层面(C/C++),而非Java类库层面(我们要清楚我们平时所称的“Java源码”其实上不是Java语言的实现,而是用Java语言编写的Java类库)。Java数组类是由JVM在运行时创建的,所以无法在JDK的Java类库中找到。
基于此,我们也可以延伸出一个话题,数据类型中的类与Java中的class
关键字有什么关系? 这里我给出个人总结的结论:数据类型中的类是指使用class
关键字声明的抽象结构。而方才程序的输出是从更底层的角度看待数据类型,在Java语言的实现层面,数组与类都是class
,Java只支持 class
和 interface
两种声明抽象结构的关键字。
笔者个人认为这个理解对于目前来说够用了,再深入的话可能需要了解一下JVM相关知识,Oop-Klass模型和类加载这些东西。
下面我们来研究另外的一些问题,数据类型名.class
为什么会产生一个类的实例?这个实例到底是什么?
Class 类
除去基本类型外,所有的 class|interface
都是由JVM在执行过程中动态加载的。JVM在第一次读取到一种 class|interface
类型(这里指类元数据,而非实例)时,将其加载入内存。
每加载一种class|interface
,JVM就为其创建一个Class
类型的实例,并关联起来。
Class
是一个Java类(准确来说,一个泛型类),类名为Class
。该类的全名为java.lang.Class
。
以String
类为例,当JVM加载String
类时,它首先读取String.class
文件到内存,然后,为String
类创建一个Class实例并关联起来,例如:
1 | // author: SilenceZheng66 |
这个Class
实例是JVM内部创建的,如果查看类库源码,可以发现Class
类的构造方法是private
,只有JVM能创建Class
实例,我们自己的Java程序是无法创建Class
实例的。
你可能会好奇String.class
在哪里,它是Java运行时环境(JRE)的一部分(如果你真的想找到它,它存在于rt.jar
中),可以理解为 String.java
编译后的结果。我们知道Java程序在运行时首先被编译为二进制Java字节码(.class文件),然后JVM负责将字节码交给解释器执行。因此JVM可以从运行环境读取String.class
文件。
所以,JVM持有的每个Class
实例都指向一个数据类型。等等,虽然我们刚刚论证了数组、类和接口都属于 class|interface
的范畴,但基本类型呢?基本类型.class
也能提供实例,但显然它们不存在对应的 .class
字节码文件,因为它们是Java的关键字!
其实,Java中有九个预定义的 Class
对象来表示八个基本类型和 void
。它们由JVM创建,并与它们所代表的原始类型具有相同的名称。这与数组有些相似,它们都是在Java的语言实现层面定义的,所以没有对应的 .class
字节码文件。不仅如此,Class
类中许多方法(如判断对象是否为接口、数组、基本类型)都是 native method
,这意味着它是使用底层语言(C/C++)实现的,而非Java语言。
解释了这个问题后,我们继续回到Class
实例上,每个 Class
实例都包含了对应数据类型的完整信息(修饰词、包名、父类、实现的接口、字段、方法…),下面是示意图:
1 | ┌───────────────────────────┐ |
如果我们想要查看这些信息,可以调用实例的对应方法:
1 | // author: SilenceZheng66 |
总之,Class
类的实例代表了运行的Java程序中的“万事万物”,所有的类和接口以及原始类型都对应某种Class
对象,枚举(类的一种)、注解(接口的一种)以及数组也有其对应的Class
对象,甚至关键字 void
也对应一个Class
对象。我们之前通过 数据类型.class
获取到的对象实际上就是 Class
对象。
因为Java引入了泛型,所以,只用
Class
来标识类型已经不够了。实际上,Java的类型系统结构如下:
1
2
3
4
5
6
7
8
9
10 ┌────┐
│Type│
└────┘
▲
│
┌────────────┬────────┴─────────┬───────────────┐
│ │ │ │
┌─────┐┌─────────────────┐┌────────────────┐┌────────────┐
│Class││ParameterizedType││GenericArrayType││WildcardType│
└─────┘└─────────────────┘└────────────────┘└────────────┘
反射(Reflection)
在了解了 Class
类后,我们可以开始尝试理解反射机制。Oracle官方对于反射的解释是这样的:
反射使 Java 代码能够发现加载类(loaded classes)的字段、方法和构造函数的信息,并使用反射的字段、方法和构造函数在安全限制内对它们的底层对应项进行操作。
从这个解释我们能够获取两个主要信息:
- 通过反射能够获取类的结构信息(字段、方法和构造函数)
- 通过反射能够操作获取到的结构信息
这似乎有点熟悉,我们刚学习了 Class
类,通过它我们可以获取任何数据类型的全部信息!事实也是如此,Class
类作为反射API中的一员,被用于提供获取 class|interface
相关信息的功能。
下面我们来看看如何获取一个类型的Class
实例:
方法一:通过class literals
(类字面量)获取。
字面量(Literal)在计算机领域通常指用于表达源代码中一个固定值的表示法。
Java中的类字面量是指形如
<class|interface|array|primitive type|void>.class
的表达式,该表达式被计算为命名类型(或 void)的Class
对象,该对象由当前实例中的 defining class loader of the class 定义。
这也正是我们一开始接触的方式:
1 | // author: SilenceZheng66 |
方法二:通过静态方法Class.forName()
获取。
这种方法需要知道类或接口的完整名(从包开始),如java.lang.Class
:
1 | // author: SilenceZheng66 |
方法三:对于实例,可以通过getClass()
方法获取。
1 | // author: SilenceZheng66 |
因为Class
实例在JVM中是唯一的,所以,上述方法获取的Class
实例是同一个实例(地址相同),可以通过 ==
运算符检验。
通过该实例获取类的相关信息我们已经接触过,下面我们来看看如何使用和操作这些信息。首先给出两个类,接下来我们将围绕它们进行操作:
1 | // author: SilenceZheng66 |
访问字段
Class
类提供了以下几个方法来获取字段:
- Field getField(name):根据字段名获取某个public的field(包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
- Field[] getFields():获取所有public的field(包括父类)
- Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
1 | // author: SilenceZheng66 |
一个 Field
实例包含了一个字段的所有信息:
- getName():返回字段名称;
- getType():返回字段类型,也是一个Class实例;
- getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义;
通过 Field
实例不仅可以获取到指定实例的字段值,还可以设置字段的值。 下面我们来一一演示。
首先获取 Class
实例的 Field
实例,并产看字段信息:
1 | // author: SilenceZheng66 |
利用反射拿到字段的一个 Field
实例后,我们还可以拿到一个实例对应的该字段的值:
1 | // author: SilenceZheng66 |
注意,以上代码不代表
private、protected
修饰语无用,反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。此外,
setAccessible(true)
可能会失败。如果JVM运行期存在SecurityManager
,那么它会根据规则进行检查,有可能阻止setAccessible(true)
。例如,某个SecurityManager
可能不允许对java和javax开头的package的类调用setAccessible(true)
,这样可以保证JVM核心库的安全。
下面我们通过 Field
实例设置指定实例的字段值:
1 | // author: SilenceZheng66 |
调用方法
Class
类提供了以下几个方法来获取 Method
实例:
- Method getMethod(name, Class…):获取某个public的Method(包括父类)
- Method getDeclaredMethod(name, Class…):获取当前类的某个Method(不包括父类)
- Method[] getMethods():获取所有public的Method(包括父类)
- Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
1 | // author: SilenceZheng66 |
使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。如 Student 类覆写的 getName 方法。
一个Method
对象包含一个方法的所有信息:
- getName():返回方法名称;
- getReturnType():返回方法返回值类型,也是一个Class实例;
- getParameterTypes():返回方法的参数类型,是一个Class数组;
- getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。
可以利用 Method
对象调用对应方法:
1 | // author: SilenceZheng66 |
对Method
实例调用invoke()
就相当于调用该方法,invoke()
第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
对于重载方法,根据参数表不同进行区分获取即可:
1 | // author: SilenceZheng66 |
如果获取到的Method
表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke()
传入的第一个参数永远为null:
1 | // author: SilenceZheng66 |
调用构造方法
除了使用new
来创建对象外,还可以通过反射来创建新的实例,比如调用Class
类提供的newInstance()
方法。
1 | Person p = Person.class.newInstance(); |
调用Class.newInstance()
的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()
来调用。
为了调用任意的构造方法,Java的反射API提供了Constructor
对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor
对象和Method
非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:
1 | // 获取构造方法Integer(int): |
通过Class
实例获取Constructor
的方法如下:
- getConstructor(Class…):获取某个public的Constructor;
- getDeclaredConstructor(Class…):获取某个Constructor;
- getConstructors():获取所有public的Constructor;
- getDeclaredConstructors():获取所有Constructor。
注意Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非public的Constructor
时,必须首先通过setAccessible(true)
设置允许访问。setAccessible(true)
可能会失败。
获取继承关系
有了Class
实例,我们还可以获取它的父类的Class
:
1 | // author: SilenceZheng66 |
以及获取实现的接口:
1 | Class s = Integer.class; |
特别注意:
getInterfaces()
只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型此外,对所有interface的
Class
调用getSuperclass()
返回的是null,获取接口的父接口要用getInterfaces()
当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof
操作符。而如果是两个Class
实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()
:
1 | // Integer i = ? |
动态加载(Dynamic Loading)
动态加载是一个可以充分深入的话题,这里也只是提供一些宏观理解。
前面我们已经提到过,JVM在执行Java程序的时候,并不是一次性把所有用到的类全部加载到内存,而是第一次需要用到时才加载。个人认为,动态加载的意义在于提供了一种提高程序灵活性和健壮性的方式。下面我尝试举例说明这一点。
首先,我们平时通过new
关键字构造对象时,需要先import
进类名。比如CLASSPATH
为dir
,我们需要的类That
源文件路径为dir/org/company/That.java
,我们就需要 import org.company.That
。这样一来,当程序进行编译时,That
类不存在于预计位置时,程序就无法通过编译。
然而,我们也可以这样写:
1 | // author: SilenceZheng66 |
这样一来,无论That
是否真实存在,程序都可以运行。其中一个应用是在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。
1 | // Commons Logging优先使用Log4j: |
反射API就是为Java语言提供了一种动态相关的机制,提高了程序的灵活性,降低了类之间的耦合性。但反射并不能随意使用,因为这类操作通常慢于直接执行语句,会造成性能影响。
动态代理(Dynamic Proxy)
由于接口不能实例化,所有interface
类型的变量总是通过某个实例向上转型并赋值给接口类型变量的。但反射就是要提供更灵活的功能,通过动态代理机制,就可以在不编写实现类的情况下,在运行时动态创建接口的实例。
一个最简单的动态代理实现如下:
1 | import java.lang.reflect.InvocationHandler; |
在运行期动态创建一个interface实例的方法如下:
- 定义一个
InvocationHandler
实例,它负责实现接口的方法调用; - 通过
Proxy.newProxyInstance()
创建接口实例,它需要3个参数:- 使用的
ClassLoader
,通常就是接口类的ClassLoader
; - 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的
InvocationHandler
实例。
- 使用的
- 将返回的
Object
强制转型为接口。
动态代理实际上是JVM在运行期动态创建class字节码并加载的过程,它并没有什么黑魔法,把上面的动态代理改写为静态实现类大概长这样:
1 | public class HelloDynamicProxy implements Hello { |
其实就是JVM帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。
参考文献
[1] https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512
[2] https://www.liaoxuefeng.com/wiki/1252599548343744/1265104600263968
[3] https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.3.3
[4] https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
[5] https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html
[6] https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/index.html
[7] https://blog.csdn.net/weixin_42621338/article/details/82684289(概念有偏差)
[8] https://blog.csdn.net/wq6ylg08/article/details/104603787
[9] https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.8.2
[10] https://scx-white.blog.csdn.net/article/details/52935472
后记
首发于 silencezheng.top,转载请注明出处。