Java反射API,位于java.lang.reflect包。顾名思义,反射是类或对象检查自身的能力。反射允许Java代码查看对象(更准确地说,对象的类)并确定其结构。在安全管理器所施加的限制内,您可以找出一个类有哪些构造函数、方法和字段,以及它们的属性。您甚至可以更改字段的值、动态调用方法和构造新对象,就像Java有指向变量和方法的原始指针一样。您可以对代码从未见过的对象执行所有这些操作。annotationsapi还能够在编译后的类中保存关于源代码的元数据,我们可以使用反射API检索这些信息。

Java反射API详解插图

我们没有足够的空间来全面介绍反射API。正如您所料,reflect包非常复杂,并且包含丰富的细节。但是反射的设计让你可以用相对较少的努力做很多事情;20%的努力给你80%的乐趣

反射API可用于在运行时确定对象的功能。对象序列化使用它来拆分和构建对象,以便通过流传输或永久存储。显然,安全管理器必须热情地保护分离对象并查看其内部结构的能力。一般规则是,您的代码不允许对反射API做任何它不能处理静态(普通的,编译的)Java代码的事情。简而言之,反射是一个强大的工具,但它不是一个自动的漏洞。默认情况下,一个对象不能使用它来处理它通常无法访问的字段或方法(例如,另一个对象的私有字段),尽管可以授予这些特权,我们将在后面讨论。

类的三个主要特性是它的字段(变量)、方法和构造函数。为了描述和访问对象,这三个特性在反射API中由单独的类表示:java.lang.reflect.Fieldjava.lang.reflect.Method,以及java.lang.reflect.Constructor。我们可以通过class对象查找类的这些成员。

类提供了两对方法来获取每种类型的特性。一对允许访问类的公共特性(包括从其超类继承的那些特性),而另一对允许访问在类中直接声明的任何公共或非公共项(但不包括继承的特性),这取决于安全性。一些例子:

  • getFields()返回一个表示类的所有公共变量(包括它继承的那些)的Field对象数组。
  • getDeclaredFields()返回一个数组,该数组表示类中声明的所有变量,而不考虑它们的访问修饰符,但不包括继承的变量。
  • 对于构造函数,“all constructor”和“declaredconstructors”之间的区别是没有意义的(类不会继承构造函数),因此getConstructors()getDeclaredConstructors()的区别只是前者返回公共构造函数,后者返回类的所有构造函数。

每一组方法都包含一次列出所有项的方法(例如getFields()),以及一个附加的方法,用于按名称查找特定项,并通过签名查找方法和构造函数(例如,getField(),它将字段名作为参数)。

下表显示了Class类中的方法:

Field [] getFields(); 获取所有公共变量,包括继承的变量。
Field getField(String name); 获取指定的公共变量,该变量可以被继承。
Field [] getDeclaredFields(); 获取该类中声明的所有公共和非公共变量(不包括从超类继承的变量)。
Field getDeclaredField(String name); 获取在此类中声明的指定变量public或nonpublic(不考虑继承的变量)。
Method [] getMethods(); 获取所有公共方法,包括继承的方法。
Method getMethod(String name, Class … argumentTypes); 获取指定的公共方法,该方法的参数与argumentTypes中列出的类型匹配。方法可以继承。
Method [] getDeclaredMethods(); 获取该类中声明的所有公共和非公共方法(不包括从超类继承的方法)。
Method getDeclaredMethod(String name, Class … argumentTypes); 获取指定的方法(public或nonpublic),该方法的参数与argumentTypes中列出的类型匹配,并且在该类中声明(不考虑继承的方法)。
Constructor [] getConstructors(); 获取此类的所有公共构造函数。
Constructor getConstructor(Class … argumentTypes);  获取此类的指定公共构造函数,该构造函数的参数与argumentTypes中列出的类型匹配。
Constructor [] getDeclaredConstructors(); 获取此类的所有公共和非公共构造函数。
Constructor getDeclaredConstructor(Class … argumentTypes); 获取指定的构造函数(public或nonpublic),该构造函数的参数与argumentTypes中列出的类型匹配。
Class [] getDeclaredClasses(); 获取该类中声明的所有公共和非公共内部类。
Constructor [] getInterfaces(); 获取由这个类实现的所有接口,按照它们的声明顺序。

如您所见,四个getMethod()getConstructor()方法利用Java可变长度参数列表,允许您传入参数类型。在旧版本的Java中,必须在它们的位置传递一个类类型数组。稍后我们将展示一个例子。

作为一个简单的例子,我们将展示列出java.util.Calendar类:

for ( Method method : Calendar.class.getMethods() )
        System.out.println( method );

在这里,我们使用.class表示法来获取对Calendar类的引用。请记住类类的讨论;反射方法不属于Calendar本身的特定实例;它们属于java.lang.Class对象,该对象描述日历类。如果我们想从Calendar的实例(或者说,一个未知对象)开始,我们可以使用该对象的getClass()方法来代替:

Method [] methods = myUnknownObject.getClass().getMethods();

修改器和安全性

Java类的所有类型的成员字段、方法、构造函数和内部类都有一个getModifiers()方法,该方法返回一组标志,指示该成员是私有的、受保护的、默认级别的还是公开可访问的。你可以用java.lang.reflect.Modifier类,如下所示:

Method method = Object.class.getDeclaredMethod( "clone" ); // no arguments
    int perms = method.getModifiers();
    System.out.println( Modifier.isPublic( perms ) ); // false
    System.out.println( Modifier.isProtected( perms ) ); // true
    System.out.println( Modifier.isPrivate( perms ) ); // false

在本例中,Object中的clone()方法是受保护的。

对反射API的访问由安全管理器控制。完全受信任的应用程序可以访问前面讨论过的所有功能;它可以在其作用域内通常授予代码的限制级别上访问类的成员。但是,可以授予对代码的特殊访问权,这样它就可以使用反射API以Java语言通常不允许的方式访问其他类的私有和受保护成员

字段、方法和构造函数类都是从名为AccessibleObject的基类扩展而来的。AccessibleObject类有一个名为setAccessible()的重要方法,它允许您在访问特定的类成员时停用常规安全性。这听起来太容易了。这确实很简单,但该方法是否允许您禁用安全性取决于Java安全管理器和安全策略。您可以在没有任何安全策略的正常Java应用程序中执行此操作,但在applet或其他安全环境中则不行。例如,为了能够使用Object类的protected clone()方法,我们所要做的就是(在不违反安全管理器的情况下):

Method method = Object.class.getDeclaredMethod( "clone" );
    method.setAccessible( true );

访问字段

java.lang.reflect.Field表示静态变量和实例变量。Field为所有基类型(例如,getInt()setInt()getBoolean()setBoolean())提供了一整套重载访问器方法,以及用于访问引用类型字段的get()set()方法。让我们来看看这个类:

class BankAccount {
        public int balance;
    }

通过反射API,我们可以读取和修改公共整数字段余额的值:

BankAccount myBankAccount = ...;
    ...
    try {
        Field balanceField = BankAccount.class.getField("balance");
        // read it
        int mybalance = balanceField.getInt( myBankAccount );
        // change it
        balanceField.setInt( myBankAccount, 42 );
    } catch ( NoSuchFieldException e ) {
        ... // there is no "balance" field in this class
    } catch ( IllegalAccessException e2) {
        ... // we don't have permission to access the field
    }

在这个例子中,我们假设我们已经知道BankAccount对象的结构。一般来说,我们可以从物体本身收集信息。

Field的所有数据访问方法都引用我们要访问的特定对象实例。在本例中,getField()方法返回一个表示BankAccount类余额的Field对象;该对象不引用任何特定的BankAccount。因此,要读取或修改任何特定的BankAccount,我们调用getInt()setInt()myBankAccount是包含我们要处理的字段的特定对象实例。对于静态字段,我们将在这里使用值null。如果我们试图访问一个不存在的字段,或者我们没有适当的读写权限,就会发生异常。如果我们将balance设置为私有字段,我们仍然可以查找描述它的field对象,但无法读取或写入其值。

因此,我们不会在编译时对静态代码做不到的事情;只要balance是我们可以访问的类的公共成员,我们就可以编写代码来读取和修改它的值。重要的是,我们在运行时访问balance,我们可以很容易地使用这个技术来检查动态加载的类中的balance字段,或者通过使用getDeclaredFields()方法迭代类的字段发现的平衡字段。

访问方法

java.lang.reflect.Method表示静态方法或实例方法。根据正常的安全规则,方法对象的invoke()方法可用于使用指定参数调用基础对象的方法。是的,Java确实有类似方法指针的东西!

作为一个例子,我们将编写一个名为Invoke的Java应用程序,它将Java类的名称和要调用的方法的名称作为命令行参数。为了简单起见,我们假设该方法是静态的,不带任何参数(相当有限):

//file: Invoke.java
    import java.lang.reflect.*;

    class Invoke {
      public static void main( String [] args ) {
        try {
          Class clas = Class.forName( args[0] );
          Method method = clas.getMethod( args[1] ); // Named method, 
                                                     // no arguments
          Object ret =  method.invoke( null );  // Invoke a static method

          System.out.println(
              "Invoked static method: " + args[1]
              + " of class: " + args[0]
              + " with no args\nResults: " + ret );
        } catch ( ClassNotFoundException e ) {
          // Class.forName() can't find the class
        } catch ( NoSuchMethodException e2 ) {
          // that method doesn't exist
        } catch ( IllegalAccessException e3 ) {
          // we don't have permission to invoke that method
        } catch ( InvocationTargetException e4 ) {
          // an exception occurred while invoking that method
          System.out.println(
              "Method threw an: " + e4.getTargetException() );
        }
      }
    }

我们可以运行invoke来获取系统时钟的值:

% java Invoke java.lang.System currentTimeMillis
    Invoked static method: currentTimeMillis of class:
    java.lang.System with no args
    Results: 861129235818

我们的第一个任务是按名称查找指定的类。为此,我们使用所需类的名称(第一个命令行参数)调用forName()方法。然后,我们请求指定方法的名称。getMethod()有两个参数:第一个参数是方法名(第二个命令行参数),第二个参数是指定方法签名的类对象数组。(请记住,任何方法都可能被重载;您必须指定签名以明确所需的版本。)因为我们的简单程序只调用不带参数的方法,所以我们创建了一个匿名的类对象空数组。如果我们想调用一个接受参数的方法,我们就会以适当的顺序传递它们各自类型的类的数组。对于基元类型,我们将使用标准包装器(IntegerFloatBoolean等)来保存值。Java中基本类型的类由各自包装器的特殊静态类型字段表示;例如,使用整数.类型对于int的类。如代码中的注释所示,从Java 5.0开始,getMethod()invoke()方法接受可变长度的参数列表,这意味着我们可以完全忽略Java的参数。

一旦我们有了方法对象,我们就调用它的invoke()方法。这将调用我们的目标方法并以对象的形式返回结果。要对该对象执行任何非平凡的操作,必须将其强制转换为更具体的对象。大概是因为我们调用了这个方法,所以我们知道需要什么样的对象。但是如果没有,我们可以使用getReturnType()方法在运行时获取返回类型的类。如果包装器是标准类型,则返回布尔类型。如果方法返回voidinvoke()将返回一个java.lang.Void对象。这是表示void返回值的“wrapper”类。

invoke()的第一个参数是我们要调用该方法的对象。如果方法是静态的,则没有对象,因此我们将第一个参数设置为null。我们的例子就是这样。第二个参数是要作为参数传递给方法的对象数组。这些类型应与调用getMethod()中指定的类型匹配。因为我们调用的方法没有参数,所以我们可以为invoke()传递第二个参数的null。与返回值一样,必须对基元参数类型使用包装类。

如果找不到或没有访问该方法的权限,则会出现前面代码中显示的异常。此外,如果被调用的方法本身抛出某种异常,则会发生InvocationTargetException。通过调用InvocationTargetExceptiongetTargetException()方法,可以找到它抛出的内容。

访问构造函数

这个java.lang.reflect.Constructor类表示对象构造函数,方法类表示方法的方式相同。当然,您可以根据安全管理器使用它来创建对象的新实例,即使构造函数需要参数。回想一下,您可以使用Class.newInstance(),但不能使用该方法指定参数。如果你真的需要的话,这就是解决这个问题的方法。

在这里,我们将创建日期类型,将字符串参数传递给构造函数:

try {
        Constructor<Date> cons =
            Date.class.getConstructor( String.class );
        Date date = cons.newInstance( "Jan 1, 2006" );
        System.out.println( date );
    } catch ( NoSuchMethodException e ) {
        // getConstructor() couldn't find the constructor we described
    } catch ( InstantiationException e2 ) {
        // the class is abstract
    } catch ( IllegalAccessException e3 ) {
        // we don't have permission to create an instance
    } catch ( InvocationTargetException e4 ) {
        // the construct threw an exception
    }

情况与方法调用的情况大致相同;毕竟,构造函数实际上只不过是具有一些奇怪属性的方法而已。我们通过传递getConstructor()来查找日期类的适当构造函数,即以单个字符串作为参数的构造函数字符串.class类型。这里,我们使用的是 Java5 变量参数语法。如果构造函数需要更多的参数,我们将传递表示每个参数的类的附加类。然后我们可以调用newInstance(),向它传递一个相应的参数对象。同样,要传递原语类型,我们首先将它们包装在它们的包装类型中。最后,我们将生成的对象打印到日期。请注意,我们在这里使用泛型插入了另一个奇怪的构造。这里的Constructor<Date>类型只允许我们专门化Date类型的构造函数,从而减少了像以前一样强制转换newInstance()方法结果的需要。

上一个示例中的异常在这里也适用,以及IllegalArgumentExceptionInstantiationException。如果类是抽象的,因此无法实例化,则抛出后者。

数组反射

反射API允许您使用java.lang.reflect.Array类。这个过程和其他类的过程非常相似,所以我们将不再详细介绍它。主要特性是一个名为newInstance()的数组静态方法,它创建一个允许您指定基类型和长度的数组。您也可以使用它来构造多维数组实例,方法是指定一个长度数组(每个维度一个)。有关更多信息,请查看您最喜欢的Java语言参考资料。

访问泛型类型信息

泛型是一个重要的附加功能,它为Java语言中的类型概念(字面上)添加了新的维度。随着泛型的添加,类型不再是简单地与Java类和接口一一对应,而是可以在一个或多个类型上参数化以创建新的泛型类型。更复杂的是,这些新类型实际上并不生成新的类,而是编译器的构件。为了保留泛型信息,Java将信息添加到编译的类文件中。

反射API可以适应所有这些,主要是通过添加新的java.lang.reflect.Type类,它能够表示泛型类型。以下代码片段可能会对您有所指导:

// public interface List<E> extends Collection<E> { ... }

    TypeVariable [] tv = List.class.getTypeParameters();
    System.out.println( tv[0].getName() ); // "E"

此代码段获取java.util.List类并打印其名称:

class StringList extends ArrayList<String> { }

    Type type = StringList.class.getGenericSuperclass();

    System.out.println( type );  //
    // "java.util.ArrayList<java.lang.String>"


    ParameterizedType pt = (ParameterizedType)type;
    System.out.println( pt.getActualTypeArguments()[0] ); //
    // "class java.lang.String"

第二个代码段获取扩展泛型类型的类的类型,然后打印对其进行参数化的实际类型。

反射访问注释数据

在本章后面,我们将讨论注释,这是一种允许将元数据添加到Java类、方法和字段中的特性。注释可以选择性地保留在编译的Java类中,并通过反射API进行访问。这是注释的几种预期用途之一,它允许代码在运行时查看元数据并为带注释的代码提供特殊服务。例如,Java对象上的属性(fieldsetter方法)可能会被注释,以表明它希望容器应用程序设置其值或以某种方式导出它。

详细介绍这一点超出了本书的范围;但是,通过反射API获取注释数据很容易。Java类以及方法和字段对象具有以下方法对(以及其他一些相关的方法):

public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
    public Annotation[] getDeclaredAnnotations()

这些方法(第一个是通用方法)返回java.lang.annotation。表示元数据的注释类型对象。

动态接口适配器

理想情况下,Java反射允许我们在运行时做所有我们可以在编译时做的事情(而不强制我们生成源代码并将其编译为字节码)。但事实并非如此。虽然我们可以在运行时使用Class.forName()方法,没有一般的方法来创建新类型的对象,这些对象没有预先存在的类文件。

这个java.lang.reflect.Proxy类通过允许创建在运行时实现任意Java接口的适配器对象,朝着解决这个问题迈出了一步。代理类是一个工厂,它可以生成适配器类,实现所需的任何接口。在适配器类上调用方法时,它们被委托给指定的InvocationHandler对象中的单个方法。您可以使用它在运行时创建任何类型接口的动态实现,并在任何地方处理方法调用。例如,使用代理,您可以记录接口上的所有方法调用,然后将它们委托给“实际”实现。这种动态行为对于使用javabean的工具非常重要,因为javabean必须注册事件侦听器。它也适用于各种各样的问题。

在下面的代码片段中,我们使用接口名称并构造实现该接口的代理。每当调用接口的任何方法时,它都会输出一条消息:

import java.lang.reflect.*;

    InvocationHandler handler =
        new InvocationHandler() {
            Object invoke( Object proxy, Method method, Object[] args ) {
                System.out.println(
                    "Method: {[QUOTE-REPLACEMENT]}+ method.getName() +"()"
                    +" of interface: "+ interfaceName
                    + " invoked on proxy!"
                );
                return null;
            }
        };

    Class clas = Class.forName( MyInterface );

    MyInterface interfaceProxy =
        (MyInterface)Proxy.newProxyInstance(
            clas.getClassLoader(), new Class[] { class }, handler );

    // use MyInterface
    myInterface.anyMethod(); // Method: anyMethod() ... invoked on proxy!

结果对象interfaceProxy被转换成我们想要的接口类型。每当调用它的任何方法时,它都会调用我们的处理程序。

首先,我们实现了InvocationHandler。这是一个具有invoke()方法的对象,该方法将被调用的方法作为其参数,以及一个表示方法调用参数的对象数组。然后我们获取要使用的接口的类Class.forName(). 最后,我们要求代理为我们创建一个适配器,指定我们想要实现的接口类型(可以指定多个)以及要使用的处理程序。invoke()将为方法调用返回正确类型的对象。如果返回错误的类型,则会引发一个特殊的运行时异常。参数或返回值中的任何基元类型都应包装在适当的包装类中。(如有必要,运行时系统将展开返回值。)

反射有什么用?

反射虽然在某种意义上是Java语言的“后门”特性,但它的用途越来越重要。在本章中,我们提到了反射用于访问运行时注释。在第22章中,我们将看到如何使用反射动态地发现JavaBean对象的功能和特性。这些都是非常专业的应用程序反射能为我们在日常生活中做些什么呢?

我们可以使用反射来执行Java具有动态方法调用和其他有用功能的功能;然而,作为一般的编码实践,动态方法调用是一个坏主意。Java的一个主要特性(以及它与一些类似语言的区别)是它强大的类型安全性。当你在反射池里泡一泡时,你就放弃了很多。尽管反射API的性能非常好,但它并不如一般的编译方法调用快。

关于反射利弊的详细分析可以参考这篇文章:https://javakk.com/728.html

更恰当的是,您可以在需要处理事先不知道的对象的情况下使用反射。反射将Java放在编程语言的更高层次,为新类型的应用开辟了可能。正如我们所提到的,它使得在运行时使用Java注释成为可能,允许我们检查类、方法和字段中的元数据。反射的另一个重要且日益增长的用途是将Java与脚本语言集成。通过反射,您可以用Java编写语言解释器,这些解释器可以访问完整的java api、创建对象、调用方法、修改变量,以及在程序运行时执行Java程序可以在编译时执行的所有其他操作。事实上,您甚至可以在Java中重新实现Java语言,允许完全动态的程序来做各种事情。