反射简介

177 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第13天,点击查看活动详情

扎实的Java开发人员应该掌握的关键技术之一是反射。这是一个非常强大的功能,但许多开发人员一开始都很难使用它——因为它似乎与大多数Java开发人员思考代码的方式格格不入。

反射是在运行时查询或内省对象并发现(和使用)它们的功能的能力。根据上下文,它可以被认为是几种不同的东西:

编程语言API

  • 一种编程风格或技术
  • 支持该技术的运行时机制
  • 语言类型系统的属性

面向对象系统中的反射是这样一种思想:编程环境可以将程序的类型和方法表示为对象。这只有在具有支持此功能的运行时的语言中才可能实现——而且这是语言的一个基本动态方面。

当使用反射风格的编程时,完全可以在不使用静态类型的情况下操作对象。这似乎是一种倒退,但如果我们不需要知道对象的静态类型就可以使用对象,那么这就意味着我们可以构建可以处理任何类型的库、框架和工具——包括编写处理代码时不存在的类型。

当Java还是一种年轻的语言时,反射是它带入主流的关键技术创新之一。尽管其他语言(特别是Smalltalk)更早地引入了它,但在Java发布时,它并不是许多语言的通用部分。

对反射的抽象描述往往令人困惑或难以掌握。让我们来看看JShell中的一些简单示例,以便更具体地了解什么是反射:

jshell> Object o = new Object();  
o ==> java.lang.Object@a67c67e  
  
jshell> Class<?> clz = o.getClass();  
clz ==> class java.lang.Object

这是我们第一次看到反射——object类型的类对象。事实上,clz的实际类型是Class,但当我们从classloading或getClass()获得一个类对象时,我们必须在泛型中使用未知类型?来处理它:

    jshell> Class<Object> clz = Object.class;  
clz ==> class java.lang.Object  
  
jshell> Class<Object> clz = o.getClass();  
| Error:  
| incompatible types: java.lang.Class<capture#1 of ? extends java.lang.Object> cannot be converted to java.lang.Class<java.lang.Object>  
| Class<Object> clz = o.getClass();

这是因为反射是一种动态的运行时机制,源代码编译器不知道真正的Class类型。这为使用反射引入了不可减少的额外复杂性——因为我们不能依赖Java类型系统来帮助我们。

另一方面,这种动态特性是反射的关键点——如果我们在编译时不知道某个东西是什么类型的话。我们必须以一种通用的方式来对待它,从而创造出建立一个开放的、可扩展的系统的灵活性。反射产生了一个基本开放的系统,这可能会与Java模块试图引入平台的更加封装的系统发生冲突。

许多熟悉的框架和开发工具都严重依赖反射来实现它们的功能,比如调试器和代码浏览器。插件体系结构、交互环境和reps也广泛使用反射。事实上,在没有反射子系统的语言中无法构建JShell。

    jshell> class Pet {  
...> public void feed() {  
...> System.out.println("Feed the pet");  
...> }  
...> }  
| created class Pet  
  
jshell> var clz = Pet.class;  
clz ==> class Pet

现在我们有了一个表示Pet类类型的对象,我们可以用它来做其他动作,比如创建一个新实例:

    jshell> Object o = clz.newInstance();  
o ==> Pet@66480dd7

问题是newInstance()返回Object -这不是一个有用的类型。我们可以将o转换回Pet,但这要求我们提前知道我们正在处理的是什么类型——这就违背了反射的动态本质;让我们试试别的:

    jshell> import java.lang.reflect.Method;  
  
jshell> Method m = clz.getMethod("feed", new Class[0]);  
m ==> public void Pet.feed()

现在我们有了一个表示方法feed()的对象——但它表示为抽象元数据——它没有附加到任何特定的实例。

对于表示方法的对象,最自然的做法就是调用它。method类定义了invoke()方法,该方法的作用是调用method对象所代表的方法。

在JShell中工作时,我们避免了大量的异常处理代码。在编写使用反射的常规Java代码时,您需要以某种方式处理可能的异常类型。

要使此调用成功,必须提供正确数量和类型的实参。这个参数列表必须包括反射性调用方法的接收者对象(假设该方法是一个实例方法)。在我们简单的例子中,它看起来是这样的:

jshell> Object ret = m.invoke(o);  
Feed the pet  
ret ==> null

除了Method对象外,反射还提供了表示Java类型系统和语言中的其他基本概念的对象,例如字段、注释和构造函数。这些类可以在java.lang.reflect包中找到——其中一些(比如构造函数)是泛型类型。

必须升级反射子系统以处理模块。由于可以反射地处理类和方法,因此还需要一个用于处理模块的反射API。key类是java.lang.Module,它可以直接从class对象访问:

var module = String.class.getModule();  
var descriptor = module.getDescriptor();

模块的描述符类型为ModuleDescriptor,提供关于模块的元数据的只读视图——相当于module-info.class的内容。

动态功能,比如发现模块,也可以在新的反射API中实现。这是通过ModuleFinder等接口实现的,但是关于如何反射地使用模块系统的详细描述超出了本文的范围。