一些基础

我们常面对的一些问题常常可以通过反射简单而优雅的解决。 没有反射,我们的解决方案会变的杂乱,笨重且脆弱。 考虑以下场景:

  • 你的项目经历打算做一个可插拔的框架,知道系统即使在构建和部署之后还是需要能够接受新的组件。 你安置了一些接口,并准备了一个修补 jar 包的机制,但你知道这些无法完全满足可插拔的需求。

  • 客户端程序已经开发有几个月了,但销售告诉你使用另一种不同的远程机制将增加销售额。 尽管使用新的远程机制是一个明智的商业决定,但是现在你必须重新实现所有的远程接口。

  • 你负责模块的公共API需要只允许来自特殊包的调用,防止外人滥用你的模块。 你为每个API调用添加了一个参数,其将保存调用类的包名。 但是现在合法用户必须改变他们的调用,而且恶意代码可以伪造包名。

这些场景说明,模块化,远程访问和安全性等等这些似乎没有太多共同点。 但确实有:其中每项都包含需求的变化,且只有根据程序结构进行决策和修改代码才能满足需求。

重新实现接口,修补JAR文件,及修改方法调用都是非乏味且机械的任务。 如此的机械,以至于你可以写一个算法来描述必要的步骤。

  1. 检查程序的结构或数据。

  2. 根据检查结构作出决策。

  3. 根据决策修改程序的行为,结构或数据。

作为程序员这些你可能非常熟悉,但是他们并不是你想象中的程序所做的任务。 最终,假如你需要适配代码则需要一个程序员坐在键盘前来完成,而不是有运行在计算机上的程序来完成。 学习反射允许你越过这一假设并让你的程序为你左这种适配。 考虑以下几个简单示例:

public class HelloWorld {
  public void printName() {
    System.out.println(this.getClass().getName());
  }
}

这一行

(new HelloWorld()).printName();

将字符串 HelloWorld 输出到标准输出。 现在假设 xHelloWorld 或其子类的实例。 这一行

x.printName();

将类名输出到标准输出。

这个小的例子更引人注目其包含前面提到的每一步。 printName 方法检查对象的类( this.getClass() )。 这样,通过委托给对象的类来决定打印什么。 实际上是通过返回的名称来决定的。 不需要重写,方法 printName 就对 HelloWorld 子类的行为与其自身不同。 printName 方法是灵活的,其适应继承 HelloWorld 的类从而导致行为改变。 当我们在范围和复杂性上构建示例时,我们将向你展示更多使用反射获得灵活性的方法。

反射的价值

反射 反射是一个运行中程序检查自身和软件环境及根据其检查结果改变其行为的能力。

为了进行自我检查,一个程序需要有自己的描述。 这就是我们称之为 元数据 的信息。 在面向对象的世界中,元数据被组织成名为 mataobjects 的对象。 元对象的运行时自我检查称为 自省

正如我们在上的面小例子看到的,自省这一步之后是行为改变。 一般反射API可以使用三种技术来使行为改变:直接修改元对象、使用元数据和 调解 (代码允许在程序执行的各个阶段调解)。 Java提供了一套丰富的使用元数据的操作,以及一些重要的调解功能。 此外,Java不允许直接修改元对象从而避免了许多复杂性。

这些特性使反射有能力让你的软件变的灵活。 使用反射编程的应用程序更容易适应不断变化的需求。 反射组件更易于被其他应用完美重用。 这些好处已在你的JDK中。

反射很强大,但也不是魔术。 为了使你的软件变得灵活你必须先掌握这门技术。 仅仅学习API的使用和其概念是不够的。 你还要必须能够区分绝对需要反射的情况和可以有益的使用反射的情况,以及应该避免反射的情况。 这本书里的例子将帮助你慧德这项技能。 另外,当你读完本书,你将会理解到目前位置阻碍反射广泛使用的三个问题。

  • 安全

  • 代码复杂性

  • 运行时性能

你将会发现对安全的关注被误导了。 Java制作是如此的精良其反射API也同样严禁的受到约束,因此安全性可以被简单的控制。 通过学习何时使用反射,何时不使用反射你将避免不表的复杂代码,这些代码通常都是使用反射不熟练的结果。 另外,你将会学习评估你的设计的性能,从而确保生成的代码满足性能要求。

这篇简介简述了反射,但几乎没有提到它的价值。 软件维护成本是开发成本的三到四倍或更多倍。 软件市场正在增加对灵活性的需求。 知道如何生产灵活的代码会增加你在市场上的价值。 反射——反思后的行为改变——是通往灵活软件的路。 反射的承诺是伟大的,它的时代已经来临。 让我们开始吧。

走进程序员乔治

乔治是一家先进的动物模拟软件公司负责野生动物组件的程序员。 在他的日常工作中,乔治每天都要面临许多挑战比如前面提到的那些。 在这本书中,我们将跟随乔治发现使用反射解决问题的好处。

乔治目前正在一个实现用户界面的团队中工作。 乔治的团队使用几个标准的Java可视化组件,其他一些是内部开发的开源组件,还有一些是从第三方获得许可的组件。 所有这些组件都集成到团队应用的用户界面中。

每个组件提供一个 setColor 方法,其接受 java.awt.Color 作为参数。 但是,这些组件层次结构的设置使得他们唯一通用基类是 java.lang.Object 。 无法使用支持 setColor 方法的公共类型引用这些组件。

这种情况给乔治的团队带来了问题。 他们只想调用 setColor 方法而不管组件的具体类型。 缺少声明 setColor 的通用类型意味着团队需要做更多的工作。 如果这个场景看起来不自然,我们邀请您探索JDK的API,看看支持相同方法但没有实现公共接口的类的数量。

选择反射

给定一个组件,团队的代码必须完成以下两个步骤: 1. 找到组件支持的 setColor 方法。 2. 调用 setColor 方法传入所需颜色。

手动完成这些步骤有很多选择。 让我们审视以下每个方法。

如果乔治团队控制所有源代码,组件就能够重构为实现声明 setColor 方法的通用接口。 那么,每个组件能够使用此接口类型引用而且 setColor 方法能在不知道具体类型的情况下调用。 然而,团队并不能控制标准Java组件或第三方组件。 即使他们修改了开源组件,开源项目也可能不会接受更改而留给团队自己额外维护。

或者团队可以为每个组件实现一个适配器。 每个这样的适配器可以实现一个通用接口并代理setColor调用到具体组件。 然而,由于团队正在使用大量的组件类,此解决方案将导致要维护的类数量激增。 另外,因为大量组件实例的存在,此解决方案将会导致系统运行时对象数量激增。 这些权衡让使用适配器成为不可取的选择。

另一种选择是使用 instanceof 和对象转换来在运行时发现具体类型,但其给乔治的团队留下了几个维护问题。 首先,代码会因为条件语句和强制转换而变得臃肿,让代码变得难以阅读和理解。 其次,代码会与每种具体类型相耦合。 这种耦合会使团队更难以添加,移除或更改组件。 这些问题让使用 instanceof 和对象转换变得不太合适。

这些选择中的每一个都涉及程序更改,而且这些更改会调整或发现组件的类型。 乔治明白只需要找到 setColor 方法并调用它就可以。 学习了一点反射知识后,它明白怎样在运行时查询一个类对象的方法。 他知道一旦找到方法就可以使用反射来调用它。 反射特别适合解决这个问题因为其解决方案不会受类型信息约束。

编写反射解决方案

为了解决团队的问题,乔治写来一个静态工具方法 setObjectColor 如下 1.1 所示。 乔治的团队可以将视觉组件和颜色一起传递给这个实用方法。 此方法查找类对象支持的 setColor 方法,并以颜色作为参数进行调用。

public static void setObjectColor(Object obj, Color color) {
  Class cls = obj.getClass(); (1)

  try {
    Method method = cls.getMethod("setColor", new Class[] {Color.class}); (2)
    method.invoke(obj, new Object[] {color}); (3)
  } catch(NoSuchMethodException ex) { (4)
    throw new IllegalArgumentException(cls.getName() + " does not support method setColor(Color)");
  } catch(IllegalAccessException ex) { (5)
    throw new IllegalArgumentException("Insufficient access permissions to call" + "setColor(:Color) in class " + cls.getName());
  } catch(InvocationTargetException ex) { (6)
    throw new RuntimeException(ex);
  }
}
1 获得类对象
2 获得方法类对象
3 obj 上调用返回的方法
4 obj 的类不支持 setColor 方法
5 无法调用 setColor 方法
6 setColor 方法抛出异常

此实用方法满足了团队能够在不知道组件具体类型的情况下设置组件颜色的需求。 该方法在不侵入任何逐渐源码的情况下实现了其目标。 它还避免了源码的膨胀,运行时内存占用膨胀,及不必要的耦合。 乔治实现了一个非常灵活有效的解决方案。

清单 1.1 中的两行使用反射来检查参数obj的结构:

(1) 这一行代码查询其类对象。
(2) 这一行向查询类中带有颜色参数的 setColor 方法。

结合起来,这两行完成了寻找要调用的 setColor 方法的第一个任务。

这些查询都是 intropection 的一种形式,是允许程序检查自身的反射特性的术语。 我们说setObjectColor introspects 它的参数 obj。 一个类的每个特性都有相应的自省形式。 接下来的几章内我们将研究每一种自省的形式。

清单 1.1 中的这一行实际影响程序的行为:

(3) 这一行在 obj 上调用返回的方法并传入颜色。

这种反射方法调用也可以称为动态调用。 动态调用 是一特性,其使程序能够在运行时调用对象上的方法,而无需在编译时指定。

在这个例子中,乔治写代码时并不知道那个 setColor 方法会被调用,因为他不知道参数 obj 的类型。 乔治写的程序在运行时通过自省来发现那个 setColor 方法可用。 动态调用使乔治的程序能够通过自省获得信息采取行动,并使解决方案发挥作用。 影响程序行为的其他反射机制将贯穿本书的其余部分。

不是所有的类都支持 setColor 方法。 静态调用 setColor 时如果对象的类不支持该方法,则编译会报错。 当使用自省时其不知道是否支持,只有到运行时才知道是否支持 setColor 方法。

(4) obj 的类不支持 setColor 方法。

对于自省代码来说,处理这种异常情况很重要。 乔治的团队保证每个视觉组件都支持 setColor 方法。 如果 obj 参数的类型不支持该方法,则他的使用程序方法被传递了一个非法参数。 他通过让 setObjectColor 抛出一个 IllegalArgumentException 来处理这个问题。

实用方法 setObjectColor 可能无法访问非公开的 setColor 方法。 另外,在动态调用期间, setColor 方法可能会抛出异常。

(5) 清单 1.1 中没有访问权限来调用 protectedprivatepackatge 之类可见性的 setColor 方法。 (6) 调用的 setColor 方法抛出异常

使用动态调用的方式完善处理这些异常情况至关重要。 简单起见,清单1.1中的代码通过在运行时异常中包装它们来处理这些异常。 当然,对于生产代码,这将被包装在一个团队取得一致意见的异常中,并在实用方法的 throws 从句中声明。

所有这些运行时处理要比强制类型转换并静态调用更费时间。 如果信息在编译时是已知的,则不需要调用自省的方法。 动态调用通过在运行时而不是编译时解析及检查要调用的方法引带来了延迟。 第9章将讨论分析技术来平衡性能及反射给你带来的巨大灵活性好处之间的权衡。

本章剩余部分聚焦必要概念来完全理解清单 1.1。 我们详细审视乔治用来让其工作的类。 我们还将讨论Java支持的要素,这些要素允许乔治这样一个灵活的解决方案。

审查正在运行的程序

反射是程序在运行时审查及改变其行为和结构的能力。 前面提到的场景已经暗示反射给程序员带来了一些令人赞叹的好处。 让我们仔细看看反射能力对Java意味着什么。

把自省想象成对着镜子看自己。 镜子为你提供了自己的形象——供你用来审查的映像/反射。 对着镜子审视自己会给你各种有用的信息,比如你穿什么衬衫配棕色裤子或者是否有绿色的东西卡在你的牙缝里。 这些信息在调整你的衣柜和卫生方面是无价的。

镜子也能告诉你关于你行为的事情。 你可以检查一个笑容看起来是否真诚或一个手势是否夸张。 这些信息对于理解怎样调整你的行为来为别人留下良好的印象至关重要。

类似的,一个程序为了自省则必须获得自身的描述。 这种自我表述是反射系统中最重要的结构元素。 通过检查自我表述,一个程序可以获得关于自身结构及行为的有效信息从而作出决策。

清单 1.1 中使用 ClassMethod 的实例来找到要调用的合适的 setColor 方法。 这些对象是Java自我描述(self-representation)的一部分。 我们将进行程序自我描述的对象称为元对象(metaobjects)。 Meta 是一个前缀通常表示 大约或超过 。 在这种情况下,元数据是保存程序信息的对象。

ClassMethod 是其实例代表程序的类。 我们称其为类的元对象或元对象类。 元对象类是Java反射API的主要组成部分。

我们称用来完成应用程序主要用途的对象称为基础对象。 在上面 setObjectColor 的示例中,调用乔治方法的应用程序以及作为参数传递给它的对象都是基础对象。 我们称程序的非反射部分为 基础程序

元对象代表正在运行的应用程序的一部分因此可以描述基本程序。 图 1.1 显示了基础对象和代表他们类的对象之间的 instanceof/实例 关系。 图 1.1 中使用的图标约定是UML。 对于不熟悉UML的读者来说,我们将在第1.7节中简要面熟这些约定。 目前,重要的是要理解该图,该图可以理解为“fido(基本级别/base level)对象是Dog的一个实例,以及一个类对象(元数据级/metalevel)。

元对象是反射式编程的一种方便的自我表示。 想象一下如果乔治试图使用源代码或字节码作为表示,他在完成任务时会有什么麻烦。 他必须解析程序甚至开始检查类的方法。 相反Java元对象提供了他所需要的所有信息,而无需额外的解析。

元对象通常还提供改变程序结构,行为和数据的方法。 在我们的示例中,乔治;使用动态调用来调用其通过自省找到的方法。 进行更改的其他反射能力包括,反射构造,动态加载和拦截方法调用。 本书展示了如何使用这些和其他机制来解决常见确困难的软件问题。

Diagram
Figure 1: Dog 是一个类对象,表示Dog类的元对象。对象 fido 是应用中一个Dog操作的一个实例。图中用依赖关系表示的 instanceof 关系将基本级别的对象连接到表示元级别类的对象。

在运行时获得方法

在我们开始时的例子中,乔治 setObjectColor 方法传递一个 Object 类型的参数 obj 。 在知道该参数的类之前,该方法无法进行任何自省。 因此,其第一步就是查询参数的类:

Class cls = obj.getClass();

getClass 方法用于在运行时访问对象的类。 getClass 方法通常用于开始反射式编程,因为许多反射式任务需要类对象。 getClass 是由 java.lang.Object 引入的,因此任何Java对象可以使用此方法获得其类对象。 getClass 方法是 final 修饰的。 这样可以放置Java程序员欺骗反射程序。 如果不是 final 修饰的,一个程序可以覆盖 getClass 方法并反回错误的类。

getClass 方法返回一个 java.lang.Object 实例。 Java用 Class 的实例来表示组成程序的类的元对象。 在本书中,我们使用术语“类对象”来表示 java.lang.Object 的实例。 类对象是最重要的元对象,因为所有Java程序都仅由类组成。

类对象提供了关于一个类的字段、方法、构造函数、和嵌套类的程序元数据。 类对象还提供关于继承层次结构的信息,并提供对反射工具的访问。 在本章中我们将聚焦代码清单1.1中Class的使用,何其基本原理。

一旦 setObjectColor 方法获得其参数的类对象,它将在该类对象中查找其要调用的方法。

Method method = cls.getMethod("setColor", new Class[] {Color.class});

查询中的第一个参数是一个包含要查询方法名的字符串,在此例中是 setColor 。 第二个参数是一个类对象数组其确定了要查询方法的参数。 在此例中我们想要一个接受一个 Color 类型参数的方法,因此我们传递给 getMethod 一个值包含一个Color类对象元素的数组。

注意,此次不使用 getClass 来提供Color的类对象。 getClass 方法对于从一个对象引用获得类对象很有用,但当我们只知道类的名字时,我们需要另一种方式。 类字面量是Java静态指定类对象的方式。 从语法上来说,任何后跟 .class 的类名都将算做其对应类对象。 在本示例中,乔治知道 setObjectColor 方法总是需要一个 Color 对象作为参数。 因此他使用 Color.class 来指定。

Class 还有其他方法可以自省。 这些方法的签名和返回值类型如表1.1所示。 如上例所示,查询使用 Class 数组来指示参数的类型。

方法

简介

Method getMethod(String name, Class[] parameterTypes)

返回一个 Method 对象其表表一个有第二个参数指定签名的目标 Class 对象的公开方法(不管是自己类中声明的还是继承来的)。

Method getMethods()

返回一个 Method 对象数组,这些对象代表目标 Class 对象支持的所有公共方法。

Method getDeclaredMethod(String name, Class[] parameterTypes)

返回一个 Method 对象其表示一个有第二个参数指定的签名的目标类对象声明的方法。

Method getDeclaredMethods()

返回一个 Method 对象数组其表示目标类对象声明的所有方法。

在查询无参数方法时,传入 null 是合法的,其表示一个零长度数组。 顾名思义,getDeclaredMethod和getDeclaredMethods返回由类显式声明的方法的方法对象。 声明的方法集不包括该类继承的方法。 但是,这二个查询返回所有可见的方法(public、protected、package、private)

查询 getMethodgetMethods 返回类的公共方法的方法对象。 这两个方法覆盖的方法即包括由类声明的方法,也包括从超类继承的方法。 但是这些查询只返回类的公开方法。

使用 getDeclaredMethod 查询类的程序员可能不小心指定了该类未声明的方法。 在此例中,抛出NoSuchMethodException异常,查询失败。 当用 getMethod 从一个类中的公开方法中查询一个方法失败时,抛出同样的异常。

在这个例子中,乔治需要找到一个方法,他使用了表1.1中的方法来查找。 一旦检索到,这些方法对象将用来访问有关方法的信息,甚至调用它们。 我们将在本章稍后详细讨论方法对象,但受限让我们仔细看看表1.1中的方法如何使用类对象。

用类对象表示类型

对表1.1中方法的讨论表明Java反射使用Class的实例来表示类型。 例如,清单1.1中的getMethod使用Class数组来指示所需方法的参数类型。 对于将对象作为参数的方法,这似乎很好,但是不是有类声明创建的类型呢?

考录清单1.2, 其中显示了 java.util.Vector 的部分代码片段。 一个方法使用接口类型作为参数,另一个使用数组,第三个使用基本数据类型。 你必须知道怎样对Vector之类的具有此类参数方法的类进行自省。

public class Vector{
  public synchronized boolean addAll( Collection c ) {}
  public synchronized void    copyInto( Object[] anArray ) {}
  public synchronized Object  get( int index ) {}
}

方法

简介

String getName()

返回目标Class对象的全限定名

Class getComponentType()

如果目标对象是数组的Class对象,则返回表示数组元素类型的Class对象

boolean isArray()

当且仅当目标目标对象表示一个数组时才返回 true

boolean isInterface()

当且仅当目标目标对象表示一个接口时才返回 true

boolean isInterface()

当且仅当目标目标对象表示一个原始类型时才返回 true

Java通过引入类对象来表示原始类型,数组和接口类型。 这些类对象无法完成许多其他类对象可以执行的所有操作。 例如,你无法为原始类型和接口创建新的实例。 但是此类类对象对于进行自省是必要的。 表1.2显示了支持表示Class类型的方法。

本节的其余部分将更详细的说明Java如何使用类对象表示原始类型,接口和数组类型。 在本节结束之前,你应该知道如何使用 getMethod 之类的方法(如清单1.2所示)对Vector.class进行自省。

表示原始类型

虽然原始类型根本就不是对象,但是Java还是使用类对象来表示所有的8个原始类型。 调用表1.1中的方法时,可以使用类字面量来指示类对象。 例如,要指定int类型可以使用 int.class 。 查询Vector类的 get 方法可以使用:

Method m  = Vector.class.getMethod("get", new Class[] {int.class});

可以使用 isPrimitive 方法来辨别一个类对象是否表示一个原始类型。

关键字 void 不是Java中的类型,其用来表示一个方法没有返回值。 但是Java确实有一个类对象表示 void 。 对于 void.class isPrimitive 方法返回true。 在1.6节,我们涵盖了方法的自省。 在自省方法的返回类型时 void.class 用来指示一个方法没有返回值。

表示接口

Java还引入了一个类对象来表示每个声明的接口。 Vector类的 addAll 方法将Collection接口的实现作为参数。 查询Vector类的 addAll 方法可以写成:

Method m = Vector.class.getMethod( "addAll", new Class[] {Collection.class} );

可以查询表示接口的类对象以获取该接口支持的方法和常量。 方法 isInterface 可以用来鉴别表示接口的类对象。

表示数组类型

Java数组是对象,但他们的类是由JVM在运行时创建。 为每个元素类型和尺寸创建一个新类。 Java数组类型同时实现了 Cloneablejava.io.Serializable 接口。

数组的类字面量和其他任何类型的字面量一样被指定。 例如,要指定一维Object数组的参数, 可以使用类字面量 Object[].class 。 查询Vector类的 copyInto 方法书写如下:

Method m = Vector.class.getMethod("copyInto", new Class[]{Object[].class});

类对象是否表示数组可以使用 ClassisArray 方法检测。 数组类型的成员变量可以使用 getComponentType 获得。 Java将多维数组视为嵌套一维数组。 因此,以下代码

int[][].class.getComponentType();

返回 int[].class 。 注意组件类型和元素和元素类型之间的区别。 对于数组类型 int[][] ,其组件类型为 int[] 而元素类型为 int[]

并非所有Java方法都像乔治的 setColor 方法接收非接口,非数组的对象参数。 在许多情况下使用表1.2的方法对 Vector 的方法进行自省很重要。 现在你理解了如何对Java方法进行自省,那么让我们来看以下获得方法对象后我们可以做什么。

解解Method对象

前几节中的大多数示例都使用了关键字 Method ,但没有对其进行解释。 Method 是表1.1中所有查询方法的返回类型,乔治使用此类型在代码清单1.1中调用了 setColor 方法。 那么, java.lang.reflect.Method 是表示方法的元对象类就不奇怪了。 表1.3显示了一些元对象类 Method 支持的方法。

Table 1. Method支持的方法

方法

描述

Class getDeclaringClass()

返回声明此 Method 对象表示的方法的类对象。

Class[] getExceptionTypes()

返回表示此方法对象表示的方法声明的异常类的对象数组

int getModifiers()

返回此 Method 对象表示的方法的修饰符转码为 int

String getName()

返回此 Method 对象代表的方法的方法名

Class[] getParameterTypes()

返回以声明顺序排序的形参类对象数组

Class getReturnType()

返回此 Method 对象表示的方法的返回值类对象。

Object invoke(Object obj, Object[] args)

在指定对象上以指定的对象数组作为参数调用此 Method 对象代表的方法

每个 Method 对象提供关于方法的方法名、参数类型、返回值类型、以及异常等信息。 一个 Method 对象同时也提供能力来调用其所表示的方法。 在我们的示例中,我们最感兴趣的是其调用方法的能力,因此此节剩余的内容我们将注意力集中于 invoke 方法。

使用动态调用

动态调用使程序可以在运行时调用一个方法,而无需在编译时指定其方法。 在1.2节乔治在写程序时不知道那个setColor方法将会被调用。 其程序依靠自省检查其参数obj的类,在运行时发现需要调用的方法。 作为自省的结果,表示 setColor 方法的 Method 对象存储在变量 method 中。

按照代码清单1.1进行自省后,此行将动态调用 setColor 方法:

method.invoke(obj, new Object[] {color});

这里变量 color 持有一个 Color 类型的变量。 此行使用 invoke 方法调用前面使用自省获得的 setColor 方法。 setColor 方法在 obj 上调用并将 color 的值作为参数传递给它。

第一个参数作为此次方法调用的目标,或者说要执行方法的对象。 乔治传入 obj 因为他像在 obj 上调用 setColor 方法。 但如果 obj 的类将 setColor 方法声明为静态方法,其第一个参数将被呼略因为静态方法不需要调用目标对象。 对于静态方法可以将null作为调用的第一个参数,而不会引发异常。

调用的第二个参数 args 是一个对象数组。 invoke 方法将此数组元素作为动态调用方法的实参。 对于没有参数的放法,第二个参数可以是零长度数组或null。

在动态调用中使用原始类型

invoke 的第二个是一个Object数组,并且其返回值也是Object。 当然,Java中的许多方法接受原始值作为参数并返回原始值。 理解如何在 invoke 方法中使用原始类型非常重要。

如果参数是原始类型, invoke 期待 args 数组的元素为原始值对应的包装类型。 例如,当调用带有 int 参数的方法时,需要将 int 参数包装于 java.lang.Integer 然后放置在 args 数组中。 invoke 方法在将参数传读给此方法调用的实际代码之前对参数进行解包。

invoke 方法通过在返回原始类型之前将他们包装起来来处理返回值。 因此,当调用具有 int 返回值类型的方法时,程序将会收到一个 Integer 类型的返回值。 如果调用方法声明为返回 voidinvoke 方法返回 null

因此,原始类型在传递给动态调用的方法时需要进行包装,而当接收其返回值时则会进行解包。 为了清楚起见,请考虑下面示例中变量 obj 动态调用 hashCode 方法。

Method method = obj.getClass().getMethod("hashCode", null);
int code = (Integer method.invoke(obj, null)).intValue();

第一行自省了无参的 hashCode 方法。 此查询不会失败因为此方法在 Object 中就有声明。 hashCode 方法返回 int 。 第二行动态调用 hashCode 并将返回值储存在变量 code 中。 注意,返回返回值包装在Integer中,并被强制转换解包。 以上代码图解见时序图1.2中。

Diagram
Figure 1.2: 使用时序图来说明 getMethod 方法调用。返回的箭头标记有返回值的类型。invoke 调用将返回的 int 值包装在包装在 Integer 对象中。

避免调用陷阱

有时,乔治想,“ 如果我有一个表示setColor的方法,为什么我需要每次都自省?我可以在第一次查询后进行缓存,并优化其余的查询。 ” 当他尝试此操作时,他从随后的 invoke 调用中得到许多 IllegalArgumentException 。 此异常消息意味着,此方法是在一个不是声明它的类的实例上调用的。

乔治优化失败了因为他假设拥有相同签名的方法为相同方法。 实际不是如此。 在Java中,每个方法都由其签名和声明类来标识。

让我们仔细审视下这个错误。 图1.3显示了 AnimalShape 类,他们都声明了具有相同签名的 setColor 方法。 这两个 setColor 方法在Java中不是相同的方法因为它们没有相同的声明类。

Diagram
Figure 1.3: 一幅UML类图。Dog是Animal的子类。Animal类和Shape类各声明了一个拥有相同签名的 setColor 方法。Java语言认为所示的两个方法是不同的方法。但是Dog的setColor方法和Animal的是相同的。

另一个类Dog扩展了Animal并继承了其setColor方法。 Dog的setColor方法和Animal的setColor方法相同,因为Dog的此方法是从Animal继承而来的。 Dog的setColor方法与Shape的不是同一个。 因此,当处理这种情况时,通常最简单的方式是每次都自省而不是使用缓存。

调用 invoke 时也会发生其他异常。 如果在类上调用 invoke 时没有适当的方法访问权限, invoke 抛出一个 IllegalAccessException 。 例如,当试图在类外部调用一个私有方法时会发生此异常。

在多种情况下均可抛出 IllegalAccessException 。 执行其类不支持的方法的调用产生一个 IllegalArgumentException 。 提供错误长度的参数数组或输入错误的类型也会产生 IllegalArgumentException 。 如果所调用的方法发生任何异常,那么该异常将被包装于 InvocationTargetException 中并抛出。

动态调用是Java反射中一个很重要的特性。 如果没有这一特性,每个方法调用必须在编译时硬编码,这样就会阻止程序员灵活的去做乔治代码清单1.1中类似的事。 在后面的章节中,我们将回到更高级程序的动态调用中,并介绍使用自省获取信息的其他强大方法。

图解反射

在这本书中,我们在像图1.4这样的图中使用UML。 熟悉UML的人可能会注意到,图1.4结合了UML类图和对象图。 反射使用元对象在运行时表示图中的所有类图实体。 因此,将类图和对象图结合,对于清晰传达反射设计很有用。

UML通常仅包含类或不对象。 建模反射需要将两者结合,并使用instanceOf依赖关系将对象和其实例化类关联起来。 UML定义了instanceOf依赖项,其意义与Java的instanceOf操作符相同。 但是,本书只使用instanceOf依赖项来表明对象是类的直接实例。 清楚起见,我们将图1.4区分为base level和metalevel,尽管该区分不是标准UML的。 关于UML的更多细节,参见附录C。

Diagram
Figure 1.4: 这是描述克隆羊Dolly的UML图。此图显示了一个对象dolly,它是Sheep类的实例。其将Sheep描述为实现了Cloneable的Mammal。关于此图最重要的是其同时包含对象和类,这对于描述反射系统是必须的。

浏览继承层次结构

当乔治的团队使用代码清单1.1中的 setObjectColor 一段时间之后,玛莎遇到了一些问题。 玛莎告诉乔治 setObjectColor 没有看到其组件继承的 setColor 方法。 探索继承结构后,乔治和玛莎发现继承的方法是受保护的,因此下面的代码没有找到该方法。

Method method = cls.getMethod("setColor", new Class[] {Color.class})

乔治意识到这需要一个可以自省所有可见的方法,声明方法,继承方法的方法。 回顾表1.1中的方法,乔治注意到没有方法左这个,因此他决定自己写一个。 代码清单1.3中显示了 getSupportedMethod 方法的源码,此方法是乔治为完成该查询而写的。 乔治将 getSupportedMethod 方法,放置在其便利设备Mopex中。 这是乔治使用的Mopex中诸多便利方法之一,并且在整本书中我们会解释并使用它们。 .代码清单1.3 Mopex.getSupportedMethod

public static Method getSupportedMethod (Class cls,
                                         String name,
                                         Class[] paramTypes)
  throws NoSuchMethodException {
    if (cls == null) {
      throw new NoSuchMethodException();
    }
    try {
      return cls.getDeclaredMethod(name, parameterTypes);
    } catch (NoSuchMethodException ex) {
      return getSupportedMethod(cls.getSuperclass(), name, paramTypes);
    }
  }

getSupportedMethod 是一个递归方法其遍历继承层次使用 getDeclaredMethod 来查找具有正确签名的方法。 其使用该行代码完成遍历。

return getSupportedMethod(cls.getSuperclass(), name, paramTypes);

方法 getSuperclass 返回表示目标类扩展类的类对象。 如果类没有使用 extends 语句, getSuperclass 返回Object的类对象。 如果 cls 表示Object, getSuperclass 返回null,并且在下一次调用 getSupportedMethod 方法时抛会出 NoSuchMethodException 异常。

现在乔治已经实现了 getSupportedMethod 方法,其执行了他想要的自省操作,他可以修改 setObjectColor 来使用此新功能。 代码清单1.4显示了 setObjectColor 方法的更新。

public static void setObjectColor(Object obj, Color color) {
  Class cls = obj.getClass();
  try {
    Method method = Mopex.getSupportedMethod(cls,
                                             "setColor",
                                             new Class[]{Color.class});
    method.invoke(obj, new Object[]{color});
  } catch (NoSuchMethodException ex) {
    throw new IllegalArgumentException(cls.getName()
                                       + " does not support"
                                       + " method setColor(:Color)"
                                       + cls.getName());
  } catch (InvocationTargetException ex) {
    throw new RuntimeException(ex);
  }
}

此更新使 setObjectColor 能够获取 getMethod 方法无法从元对象获取的private、package和protected方法。 但是,此更新不保证有权限来调用这些方法。 如果 setObjectColor 无法访问继承的方法,并且抛出 IllegalAccessException 异常而不是 NoSuchMethodException

乔治刚刚发现一种反射方式可以节省其精力。 在进行反射增强前,他和玛莎需要探索继承层次结构来来诊断玛莎的问题。 乔治增强遍历继承层次结构并报告了问题,从而避免了麻烦。 在第二章,我们将讨论使用反射绕过可见性检查。 现在,让我们继续讨论此工具能为乔治和玛莎带来的可能的增强。

内省继承层次

如上一节所示,运行时访问关于继承层次结构的信息可以防止额外的工作。 获取类的父类只是Java反射提供的用于处理继承层次的众多操作之一。 表1.4展示了Class类中声明的关于继承的方法的签名和返回值类型,和接口实现。

方法

简介

Class[] getInterfaces()

返回一个包含 Class 对象数组,其表表目标 Class 对象的直接父接口。

Class getSuperclass()

返回表示目标 Class 对象的直接父类 Class 对象或null(如果父类是Object的话)。

boolean isAssignableFrom(Class cls)

当且仅当目标对象表示的类或接口与指定的 Class 参数相同或与其父类或父接口相同时返回 true

boolean isInstance(Object obj)

当且仅当指定对象与目标类对象表示的对象赋值兼容时返回 true

getInterfaces 方法返回表示接口的类对象。 当在表示类的类对象上调用时, getInterfaces 返回类声明中 implements 子句中指定的接口的类对象。 当在表示接口的类对象上调用时, getInterfaces 返回接口定义中 extends 子句指定接口的类对象。

注意 getInterfacesgetSuperclass 方法中的命名与Java语言规范中定义的术语稍有不同。 直接父类是指类声明中 extends 子句中命名的类。 如果从Y到X有多一个或多个直接父类链接组成的序列,那么X是Y的父类。 直接父接口和父接口有对应的定义。 Consequently, getSuperclass return the direct superclass and getInterfaces returns the direct superinterfaces. 因此, getSuperclass 返回父类, getInterfaces 返回直接父接口。

获取一个类的所有方法,程序必须遍历继承层次结构。 幸运的是,对于查询类对象是不是另一个类对象的子类来说此步骤不是不许的。 此操作可以使用 isAssignableFrom 方法来完成。 isAssignableFrom 这个名字有点令人困惑。 下面示例便于我们思考:

X.isAssignableFrom(Y)

as "an X field can be assigned a value from a Y field." 大致意思是“X类型的字段可以赋一个Y类型的值”。 例如如下代码均返回 true

Object.class.isAssignableFrom(String.class)
java.util.List.class.isAssignableFrom(java.util.Vector.class)
double.class.isAssignableFrom(double.class)

但以下代码返回 false

Object.class.isAssignableFrom(double.class)

isInstance 方法是 instanceof 的Java反射动态版本。 如果目标类对象表示一个类,且其参数是一个此类或此类它类任意子类的实例 isInstance 返回 true 。 如果目标类对象表示一个接口, 且其参数的类实现了此接口或此接口的其他任意子接口 isInstance 返回 true

透露一些惊喜

在Java反射API中,有些关系乍一看让人感到惊讶。 我们现在讨论这些关系将为我们在本书及一般的反射编程中做好准备。 这样做好准备可以更好的进行反射式编程。

isInstance 方法可以用来展示Java反射API中关于参数类对象的有趣事实。 下面这行代码:

Class.class.isInterface(Class.class)

返回 true 。 这意味着此类对象是Class自己的一个实例,从而产生图1.5中的 instanceOf`循环依赖。 `Class 是元类的一个示例,是一个用来描述实例是类的类的术语。 Class 是Java唯一的元类。

在Java中,所有对象都有一个实例化的 Class ,并且所有类都是对象。 没有循环依赖,则系统必须支持无限高塔的类对象,每一个都是其上一个的实例。 Java使用循环来解决这个问题。

Diagram
Figure 1.5: 对象fido是Dog类的实例。Dog是Class类的实例。Class同样是Class的实例。Class是元类因为它是一个类其实例也是类。

表1.5中呈现的循环性让人们感到不舒服,因为我们本能的不信任循环定义。 但是作为程序员我们熟悉其他类型的循环定义。 例如,递归。 使用递归的方法,是根据自身来定义的。 当正确使用时,递归工作良好。 同样, java.lang.Class 的定义同样有一些约束,这些约束使这种循环工作的很好。

关于递归的更多信息,查看 Putting Metaclasses to Work 。 《Putting Metaclasses to Work》是一本关于反射和元对象原型的高阶书籍由本书作者之一书写。 对于那些对反射理论和基础概念感兴趣的读者是很好的资源。

其他反射循环

将继承添加到前面的图中我们将获得图1.6中的排布。 继承为图添加了更多的循环。 Object 是一个 Class 的实例,其可以被验证,因为下面的行返回 true

Diagram
Figure 1.6: Object处于Java继承层次顶端,因此元对象类,包括Class,都是Object的子类。这意味着Object的方法是反射API的一部分。所有Java类都是其唯一的元类Class的实例。这两个条件在图中创建了一个循环。
Class.class.isInstance(Object.class)

Class是Object的子类,可以按如下代码验证:

Object.class.isAssignableFrom(Class.class)

其总是返回 true 。 概念上,在Java中我们已经知道了这些事实,每个对象有一个实例化的Class并且所有Class都属于Object。 然而,令人欣慰的是,反射模型与我们前面对语言的理解一致。

新的循环意味着 ClassObject 上的附加约束。 这些约束在JVM加载 java.lang 包时确立。 Again,a full explanation of the constraints may be found in Putting Metaclasses to Work 同样,关于这些限制的完整解释可以在《Putting Metaclasses to Work》中找到。

图1.6也说明了为什么 Object 是反射API的一部分。 所有元对象继承自 Object ,并且其继承了其方法。 因此,其每个方法都可用于反射式编程。

总结

反射允许程序来审查它们自己并在运行时改变其结构和行为。 即使简单使用反射也允许程序员写代码来做程序员平时做的事情。 这些简单的方法包括获取对象的类,检查类的方法,在运行时发现及调用方法,及浏览继承结构。

元对象类 ClassMethod 表示运行时程序的类和方法。 其他元对象表示程序的其它部分例如字段,调用栈,类加载器。 Class 有额外的方法来支持其他元对象。 从这些元对象查询信息被称为自省。

元对象也提供改变程序结构和行为的能力。 使用动态调用,一个 Method 元对象可以控制调用其表示的方法。 反射提供类一些其它方式来影响一个程序的行为和结构,例如反射访问,修改,构造及动态加载。

这里有几个使用反射解决问题的例子。 一个反射解决方案通常以从元对象查询运行中程序的信息开始。 之后使用自省采集信息,一个反射程序使用这些信息来改变此程序的行为。

每个新元对象允许我们扩展示例的范围和数量。 这些示例揭示了我们学到的教训和我们使用的技术。 他们每一个都包含相同的模式,通过内省获取信息,然后使用这些信息以某种方式来改变程序。