corejava11(5.7 反射)

5.7 反射

反射库为您提供了一个非常丰富和精细的工具集来编写程序,动态地操纵Java代码。使用反射,Java可以支持用户界面生成器、对象关系映射器以及许多动态查询类的能力的其他开发工具。

可以分析类功能的程序称为反射程序。反射机制非常强大。正如下一节所示,您可以使用它

  • 在运行时分析类的功能
  • 例如,在运行时检查对象,以编写适用于所有类的单个toString方法
  • 实现通用数组操作代码
  • 利用像C++中的函数指针一样的Method对象

反射是一种强大而复杂的机制;然而,它主要对工具构建者感兴趣,而不是应用程序程序员。如果您感兴趣的是编程应用程序,而不是其他Java程序员的工具,那么您可以安全地跳过本章的其余部分,稍后再返回。

5.7.1 Class类

在运行程序时,Java运行时系统总是维护所有对象上的运行时类型标识。此信息跟踪每个对象所属的类。虚拟机使用运行时类型信息来选择要执行的正确方法。

但是,您也可以通过使用特殊Java类来访问此信息。保存这些信息的类被称为Class,有点令人困惑。Object类中的getClass()方法返回Class类型的实例。

Employee e;
...
Class cl = e.getClass();

就像Employee对象描述特定雇员的属性一样,Class对象描述特定类的属性。类中最常用的方法可能是getName。这将返回类的名称。例如,语句

System.out.println(e.getClass().getName() + " " + e.getName());

打印

Employee Harry Hacker

如果e是一个employee,或者

Manager Harry Hacker

如果e是一个manager。

如果类是在一个包里面,包名是类名的一部分:

var generator = new Random();
Class cl = generator.getClass();
String name = cl.getName(); // name is set to "java.util.Random"

您可以使用静态forName方法获得与类名对应的Class对象。

String className = "java.util.Random";
Class cl = Class.forName(className);

如果类名存储在运行时变化的字符串中,请使用此方法。如果className是一个类或接口的名称,这就可以工作。否则,forName方法将抛出选中的异常。参见第267页第5.7.2节“声明异常的入门”,了解如何在使用此方法时提供异常处理程序。

提示

启动时,将加载包含main方法的类。它加载它需要的所有类。每个加载的类都加载它需要的类,依此类推。对于一个大型应用程序来说,这可能需要很长的时间,这会让用户感到沮丧。你可以用下面的技巧给你程序的用户一个快速启动的假象。确保包含主方法的类没有显式引用其他类。在其中,显示一个启动屏幕。然后通过调用class.forName手动强制加载其他类。

第三种获取类型Class的一个对象是方便的速记法。如果T是任何Java类型(或者void关键字),那么T.class就是匹配的类对象。例如:

Class cl1 = Random.class; // if you import java.util.*;
Class cl2 = int.class;
Class cl3 = Double[].class;

请注意,Class对象确实描述了一个类型,它可能是类,也可能不是类。例如,int不是类,但int.class仍然是Class类型的对象。

注意

Class类实际上是一个泛型类。例如,Employee.class的类型为Class<Employee>。我们并没有详细讨论这个问题,因为它会使一个已经抽象的概念进一步复杂化。对于大多数实际用途,可以忽略类型参数并使用原始Class类型。有关此问题的更多信息,请参阅第8章。

小心

出于历史原因,getName方法为数组类型返回一些奇怪的名称:

  • Double[].class.getName()返回"[Ljava.lang.Double;"
  • int[].class.getName()返回"[I"。

虚拟机为每种类型管理一个唯一的Class对象。因此,可以使用==运算符比较类对象。例如:

if (e.getClass() == Employee.class) ...

如果e是Employee的实例,则此测试通过。与条件e instanceof Employee不同,如果e是子类(如Manager)的实例,则此测试将失败。

If you have an object of type Class, you can use it to construct instances of

the class. Call the getConstructor method to get an object of type
Constructor, then use the newInstance method to construct an
instance. For example:

如果您有一个Class类型的对象,您可以使用它来构造该类的实例。调用getConstructor方法获取Constructor类型的对象,然后使用newInstance方法构造实例。例如:

var className = "java.util.Random"; // or any other name of a class
                                    // a noarg constructor
Class cl = Class.forName(className);
Object obj = cl.getConstructor().newInstance();

如果类没有无参数的构造函数,则getConstructor方法将引发异常。您将在第286页的第5.7.7节“调用任意方法和构造函数”中看到如何调用其他构造函数。

注意

有一个已弃用的Class.toInstance方法,它还构造了一个不带参数构造函数的实例。但是,如果构造函数抛出一个已检查的异常,则该异常将在不检查的情况下重新引发。这违反了异常的编译时检查。相反,Constructor.newInstance将任何构造函数异常包装为InvocationTargetException。

C++注意

The newInstance method corresponds to the idiom of a virtual
constructor in C++. However, virtual constructors in C++ are not a
language feature but just an idiom that needs to be supported by a
specialized library. The Class class is similar to the type_info
class in C++, and the getClass method is equivalent to the
typeid operator. The Java Class is quite a bit more versatile than
type_info, though. The C++ type_info can only reveal a

string with the name of the type, not create new objects of that type.

newInstance方法对应于C++中的虚拟构造函数语法。然而,C++中的虚拟构造函数不是语言特征,而是一个需要由专门库支持的语法。Class类与C++中的type_info类类似,getClass方法相当于typeid运算符。不过,Java类比type_info更通用。C++ type_info只能显示一个具有类型名称的字符串,而不是创建该类型的新对象。

java.lang.Class 1.0

  • static Class forName(String className)
    返回表示名为className的类的Class对象。
  • Constructor getConstructor(Class… parameterTypes) 1.1
    生成一个用给定参数类型描述构造函数的对象。有关如何提供参数类型的更多信息,请参见第286页的第5.7.7节“调用任意方法和构造函数”。

java.lang.reflect.Constructor 1.1

  • Object newInstance(Object… params)
    构造构造函数声明类的新实例,将params传递给构造函数。有关如何提供参数的更多信息,请参见第286页的第5.7.7节“调用任意方法和构造函数”。

java.lang.Throwable 1.0

  • void printStackTrace()
    将Throwable对象和堆栈跟踪打印到标准错误流。

5.7.2 声明异常的入门

我们在第7章中详细介绍了异常处理,但在此期间,您偶尔会遇到威胁抛出异常的方法。

当运行时发生错误时,程序可以“抛出异常”。抛出异常比终止程序更灵活,因为您可以提供一个处理程序来“捕获”异常并进行处理。

如果不提供处理程序,程序将终止并向控制台打印消息,给出异常的类型。当意外使用空引用或超出数组边界时,您可能已经看到异常报告。

有两种异常:未检查的异常和检查的异常。对于检查的异常,编译器会检查您(程序员)是否知道该异常并准备好处理结果。但是,许多常见的异常(如边界错误或访问空引用)都是未检查的。编译器并不期望您提供一个处理程序,毕竟,您应该将精力花在避免这些错误上,而不是为它们编写处理程序。

但并非所有的错误都是可以避免的。如果一个异常可能发生,尽管您尽了最大努力,那么大多数Java API将抛出一个检查异常。一个例子是Class.forName方法。无法确保具有给定名称的类存在。在第7章中,您将看到一些异常处理策略。现在,我们只向您展示最简单的策略。

每当方法包含可能引发检查异常的语句时,请向方法名添加throws子句。

public static void doSomethingWithClass(String name)
throws ReflectiveOperationException
{
    Class cl = Class.forName(name); // might throw exception do something with cl
}

调用此方法的任何方法也需要throws声明。这包括main方法。如果实际发生异常,main方法将以堆栈跟踪终止。(您将在第7章中学习如何捕获异常,而不是让它们终止您的程序。)

您只需要为检查异常提供throws子句。当您调用一个方法,很容易找出哪些方法抛出了检查异常——该方法威胁要抛出一个检查异常,而您不提供处理程序时,编译器就会抱怨。

5.7.3 资源

类通常具有关联的数据文件,例如:

  • 图像和声音文件
  • 带有消息字符串和按钮标签的文本文件

在Java中,这样的关联文件称为资源。

例如,考虑一个显示消息的对话框,如图5.3所示。

图5.3 显示图像和文本资源

当然,小组中的书名和版权年份将更改为下一版本的书。为了便于跟踪此更改,我们将把文本放在文件中,而不是将其硬编码为字符串。

但是,您应该将about.txt等文件放在哪里?当然,只需将它与其他程序文件放在JAR文件中就很方便了。

Class类为查找资源文件提供了一个有用的服务。以下是必要的步骤:

  1. 获取具有资源的类的Class对象,例如ResourceTest.class。

  2. 有些方法(例如ImageIcon类的getImage方法)接受描述资源位置的URL。然后你调用

    URL url = cl.getResource("about.gif");
    
  3. 否则,使用getResourceAsStream方法获取用于读取文件中数据的输入流

关键在于Java虚拟机知道如何定位类,因此它可以在同一位置搜索相关的资源。例如,假设ResourceTest类在包resources中。然后,ResourceTest.class文件位于resources目录中,并将图标文件放在同一目录中。

您可以提供一个相对或绝对路径,例如

data/about.txt
/corejava/title.txt

自动加载文件是资源加载功能所做的全部工作。没有解释资源文件内容的标准方法。每个程序必须有自己的资源文件解释方式。

资源的另一个常见应用是程序的国际化。依赖于语言的字符串(如消息和用户界面标签)存储在资源文件中,每种语言一个文件。第二卷第7章讨论的国际化API支持组织和访问这些本地化文件的标准方法。

清单5.13是一个演示资源加载的程序。(不要担心用于读取文本和显示对话框的代码,我们稍后将介绍这些详细信息。)编译、构建JAR文件并执行它:

javac resource/ResourceTest.java
jar cvfe ResourceTest.jar resources.ResourceTest \
	resources/*.class resources/*.gif resources/data/*.txt corejava/*
java -jar ResourceTest.jar

将JAR文件移动到另一个目录,然后再次运行它,以检查程序是否从JAR文件读取资源文件,而不是从当前目录读取资源文件。

清单5.13 resources/ResourceTest.java

package resources;

import java.io.*;
import java.net.*;
import java.nio.charset.*;
import javax.swing.*;

/**
 * @version 1.5 2018-03-15
 * @author Cay Horstmann
 */
public class ResourceTest
{
   public static void main(String[] args) throws IOException
   {
      Class cl = ResourceTest.class;
      URL aboutURL = cl.getResource("about.gif");
      var icon = new ImageIcon(aboutURL);

      InputStream stream = cl.getResourceAsStream("data/about.txt");
      var about = new String(stream.readAllBytes(), "UTF-8");

      InputStream stream2 = cl.getResourceAsStream("/corejava/title.txt");      
      var title = new String(stream2.readAllBytes(), StandardCharsets.UTF_8).trim();

      JOptionPane.showMessageDialog(null, about, title, JOptionPane.INFORMATION_MESSAGE, icon);
   }
}

java.lang.Class 1.0

  • URL getResource(String name) 1.1
  • InputStream getResourceAsStream(String name) 1.1
    在类所在的位置查找资源,然后返回可用于加载资源的URL或输入流。如果找不到资源,则返回null,因此不会引发I/O错误异常。

5.7.4 使用反射分析类的能力

下面是反射机制中最重要的部分的简要概述,让您检查类的结构。

java.lang.reflect包中的三个类Field、Method和Constructor分别描述了类的字段、方法和构造函数。这三个类都有一个名为getName的方法,该方法返回项目的名称。Field类有一个getType方法,它返回一个描述字段类型的对象(也是Class类型)。Method类和Constructor函数类具有报告参数类型的方法,Method类还报告返回类型。所有这三个类都有一个名为getModifiers的方法,该方法返回一个整数,其中打开和关闭了各种位,描述使用的修饰符,如public和static。然后,可以使用java.lang.reflect包中修饰符类中的静态方法分析getModifiers返回的整数。在修饰符类中使用isPublic、isPrivate或isFinal等方法来判断方法或构造函数是public、private还是final的。您所要做的就是在Modifier类中对getModifiers返回的整数使用适当的方法。也可以使用Modifier.toString方法打印修改器。

Class类的getFields、getMethods和getConstructors方法返回类支持的public字段、方法和构造函数的数组。这包括超类的public成员。Class类的getDeclaredFields、getDeclaredMethods和getDeclaredConstructors方法返回由类中声明的所有字段、方法和构造函数组成的数组。这包括private,package以及protected成员,但不包括超类成员。

清单5.14展示了如何打印出关于类的所有信息。程序提示您输入类的名称,并写出所有方法和构造函数的签名以及类的所有实例字段的名称。例如,如果输入

java.lang.Double

程序打印出

public class java.lang.Double extends java.lang.Number
{
    public java.lang.Double(java.lang.String);
    public java.lang.Double(double);
    public int hashCode();
    public int compareTo(java.lang.Object);
    public int compareTo(java.lang.Double);
    public boolean equals(java.lang.Object);
    public java.lang.String toString();
    public static java.lang.String toString(double);
    public static java.lang.Double valueOf(java.lang.String);
    public static boolean isNaN(double);
    public boolean isNaN();
    public static boolean isInfinite(double);
    public boolean isInfinite();
    public byte byteValue();
    public short shortValue();
    public int intValue();
    public long longValue();
    public float floatValue();
    public double doubleValue();
    public static double parseDouble(java.lang.String);
    public static native long doubleToLongBits(double);
    public static native long doubleToRawLongBits(double);
    public static native double longBitsToDouble(long);
    public static final double POSITIVE_INFINITY;
    public static final double NEGATIVE_INFINITY;
    public static final double NaN;
    public static final double MAX_VALUE;
    public static final double MIN_VALUE;
    public static final java.lang.Class TYPE;
    private double value;
    private static final long serialVersionUID;
}

这个程序的显著之处在于它可以分析Java解释器可以加载的任何类,而不仅仅是编译程序时可用的类。我们将在下一章中使用这个程序来窥探Java编译器自动生成的内部类。

清单5.14 reflection/ReflectionTest.java

package reflection;

import java.util.*;
import java.lang.reflect.*;

/**
 * This program uses reflection to print all features of a class.
 * @version 1.11 2018-03-16
 * @author Cay Horstmann
 */
public class ReflectionTest
{
   public static void main(String[] args)
         throws ReflectiveOperationException      
   {
      // read class name from command line args or user input
      String name;
      if (args.length > 0) name = args[0];
      else
      {
         var in = new Scanner(System.in);
         System.out.println("Enter class name (e.g. java.util.Date): ");
         name = in.next();
      }

      // print class name and superclass name (if != Object)
      Class cl = Class.forName(name);
      Class supercl = cl.getSuperclass();
      String modifiers = Modifier.toString(cl.getModifiers());
      if (modifiers.length() > 0) System.out.print(modifiers + " ");
      System.out.print("class " + name);
      if (supercl != null && supercl != Object.class) System.out.print(" extends "
            + supercl.getName());

      System.out.print("\n{\n");
      printConstructors(cl);
      System.out.println();
      printMethods(cl);
      System.out.println();
      printFields(cl);
      System.out.println("}");
   }

   /**
    * Prints all constructors of a class
    * @param cl a class
    */
   public static void printConstructors(Class cl)
   {
      Constructor[] constructors = cl.getDeclaredConstructors();

      for (Constructor c : constructors)
      {
         String name = c.getName();
         System.out.print("   ");
         String modifiers = Modifier.toString(c.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");         
         System.out.print(name + "(");

         // print parameter types
         Class[] paramTypes = c.getParameterTypes();
         for (int j = 0; j < paramTypes.length; j++)
         {
            if (j > 0) System.out.print(", ");
            System.out.print(paramTypes[j].getName());
         }
         System.out.println(");");
      }
   }

   /**
    * Prints all methods of a class
    * @param cl a class
    */
   public static void printMethods(Class cl)
   {
      Method[] methods = cl.getDeclaredMethods();

      for (Method m : methods)
      {
         Class retType = m.getReturnType();
         String name = m.getName();

         System.out.print("   ");
         // print modifiers, return type and method name
         String modifiers = Modifier.toString(m.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");         
         System.out.print(retType.getName() + " " + name + "(");

         // print parameter types
         Class[] paramTypes = m.getParameterTypes();
         for (int j = 0; j < paramTypes.length; j++)
         {
            if (j > 0) System.out.print(", ");
            System.out.print(paramTypes[j].getName());
         }
         System.out.println(");");
      }
   }

   /**
    * Prints all fields of a class
    * @param cl a class
    */
   public static void printFields(Class cl)
   {
      Field[] fields = cl.getDeclaredFields();

      for (Field f : fields)
      {
         Class type = f.getType();
         String name = f.getName();
         System.out.print("   ");
         String modifiers = Modifier.toString(f.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");         
         System.out.println(type.getName() + " " + name + ";");
      }
   }
}

java.lang.Class 1.0

  • Field[] getFields() 1.1
  • Field[] getDeclaredFields() 1.1
    getFields返回一个数组,其中包含此类或其超类的公共字段的Field对象;getDeclaredField返回此类所有字段的Field对象数组。如果没有此类字段,或者Class对象表示基元或数组类型,则方法返回长度为0的数组。
  • Method[] getMethods() 1.1
  • Method[] getDeclaredMethods() 1.1
    返回包含Method对象的数组:getMethods返回公共方法并包含继承的方法;getDeclaredMethods返回此类或接口的所有方法,但不包括继承的方法。
  • Constructor[] getConstructors() 1.1
  • Constructor[] getDeclaredConstructors() 1.1
    返回一个包含Constructor对象的数组,该数组为您提供该类对象表示的类的所有public构造函数(对于getConstructors)或所有构造函数(对于getDeclaredConstructors)。
  • String getPackageName() 9
    获取包含此类型的包的名称,或者如果该类型是数组类型,则元素类型的包,或者“java.lang”,如果该类型是基元类型。

java.lang.reflect.Field 1.1
java.lang.reflect.Method 1.1
java.lang.reflect.Constructor 1.1

  • Class getDeclaringClass()
    返回定义此构造函数、方法或字段的类的Class对象。
  • Class[] getExceptionTypes()(在Constructor以及Method类)
    返回表示方法引发的异常类型的Class对象数组。
  • int getModifiers()
    返回描述此构造函数、方法或字段的修饰符的整数。使用Modifier类中的方法来分析返回值。
  • String getName()
    返回一个字符串,该字符串是构造函数、方法或字段的名称。
  • Class[] getParameterTypes() (在Constructor和Method类中)
    返回表示参数类型的Class对象数组。
  • Class getReturnType() (在Method类中)
    返回表示返回类型的Class对象。

java.lang.reflect.Modifier 1.1

  • static String toString(int modifiers)
    返回一个字符串,其中包含与修饰符中的位集相对应的修饰符。
  • static boolean isAbstract(int modifiers)
  • static boolean isFinal(int modifiers)
  • static boolean isInterface(int modifiers)
  • static boolean isNative(int modifiers)
  • static boolean isPrivate(int modifiers)
  • static boolean isProtected(int modifiers)
  • static boolean isPublic(int modifiers)
  • static boolean isStatic(int modifiers)
  • static boolean isStrict(int modifiers)
  • static boolean isSynchronized(int modifiers)
  • static boolean isVolatile(int modifiers)

测试与方法名中的修饰符对应的modifiers值中的位。

5.7.5 在运行时使用反射分析对象

在前面的部分中,我们看到了如何查找任何对象的数据字段的名称和类型:

  • 获取相应的Class对象。
  • 在Class对象上调用getDeclaredFields。

在本节中,我们将进一步了解字段的内容。当然,在编写程序时,很容易看到对象的特定字段的内容,对象的名称和类型是已知的。但是反射允许您查看编译时不知道的对象的字段。

实现这一点的关键方法是Field类中的get方法。如果ffield类型的对象(例如,从getDeclaredFields获取的对象),而obj是其中f是字段的类的对象,则f.get(obj)返回值为obj字段当前值的对象。这都有点抽象,所以让我们来看一个例子。

var harry = new Employee("Harry Hacker", 50000, 10, 1, 1989);
Class cl = harry.getClass();
	// the class object representing Employee
Field f = cl.getDeclaredField("name");
	// the name field of the Employee class
Object v = f.get(harry);
	// the value of the name field of the harry object, i.e.,
	// the String object "Harry Hacker"

当然,您也可以设置可以得到的值。调用f.set(obj, value)将对象objf表示的字段设置为新值。

实际上,这个代码有问题。由于name字段是私有字段,get和set方法将引发IllegalAccessException。只能对可访问字段使用get和set。Java的安全机制使您可以发现对象具有哪些字段,但除非您有权限,否则它不允许您读取和写入这些字段的值。

反射机制的默认行为是尊重Java访问控制。但是,可以通过在Field、Method或Constructor对象上调用setAccessible方法来重写访问控制。例如:

f.setAccessible(true); // now OK to call f.get(harry)

setAccessible方法是AccessibleObject类的一个方法,它是Field、Method和Constructor类的公共超类。这个特性是为调试程序、持久存储和类似的机制提供的。在本节后面的部分中,我们将它用于泛型toString方法。

如果未授予访问权,则对setAccessible的调用将引发异常。模块系统(第二卷第9章)或安全管理器(第二卷第10章)可拒绝访问。安全管理器的使用并不常见。但是,对于Java 9,每个程序都包含模块,因为Java API是模块化的。

因为许多库都使用反射,所以Java 9和10只在使用反射来访问模块内的非公共特性时发出警告。例如,本节末尾的示例程序将研究ArrayList和Integer对象的内部。运行程序时,控制台中会出现以下不祥的消息:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by objectAnalyzer.ObjectAnalyzer
/books/cj11/code/v1ch05/bin/) to field java.util.ArrayList.serialV
WARNING: Please consider reporting this to the maintainers of
objectAnalyzer.ObjectAnalyzer
WARNING: Use --illegalaccess=warn to enable warnings of further illegal
reflective access operations
WARNING: All illegal access operations will be denied in a future re

现在,您可以停用警告。您需要将java.base模块中的java.utiljava.lang包“打开”到“未命名模块”。详细信息请参见第二卷第9章。语法如下:

java --add-opens java.base/java.util=ALL-UNNAMED \
    --add-opens java.base/java.lang=ALL-UNNAMED \
    objectAnalyzer.ObjectAnalyzerTest

或者,您可以通过运行以下代码来查看程序在Java的未来版本中的行为:

java --illegal-access=deny objectAnalyzer/ObjectAnalyzerTest

然后,程序只会因IllegalAccessException而失败。

注意

将来的库可能会使用变量句柄而不是反射来读取和写入字段。VarHandle类似于Field。您可以使用它来读取或写入特定类的任何实例的特定字段。但是,要获取VarHandle,库代码需要一个Lookup对象:

public Object getFieldValue(Object obj, String fieldName, Lookup lookup)
throws NoSuchFieldException, IllegalAccessException
{
    Class<?> cl = obj.getClass();
    Field field = cl.getDeclaredField(fieldName);
    VarHandle handle = MethodHandles.privateLookupIn(cl, lookup)
    	.unreflectVarHandle(field);
    return handle.get(obj);
}

如果Lookup对象是在具有访问字段权限的模块中生成的,则可以执行此操作。模块中的某些方法只调用MethodHandles.lookup(),它生成一个封装调用者访问权限的对象。这样,一个模块可以授予访问另一个模块的私有成员的权限。实际问题是如何以最少的麻烦授予这些权限。

虽然我们仍然可以这样做,但是让我们看看一个适用于任何类的通用toString方法(参见清单5.15)。泛型toString方法使用getDeclaredFields获取所有数据字段,而setAccessible方便方法使所有字段都可访问。对于每个字段,它获取名称和值。通过递归调用toString,每个值都会变成一个字符串。

泛型toString方法需要解决一些复杂问题。引用循环可能导致无限递归。因此,ObjectAnalyzer跟踪已访问的对象。此外,要查看数组内部,需要使用不同的方法。您将在下一节中了解详细信息。

您可以使用此toString方法查看任何对象的内部。例如,调用

var squares = new ArrayList<Integer>();
for (int i = 1; i <= 5; i++) squares.add(i * i);
System.out.println(new ObjectAnalyzer().toString(squares));

生成输出

java.util.ArrayList[elementData=class java.lang.Object[]
{java.lang.Integer[value=1][][],
java.lang.Integer[value=4][][],java.lang.Integer[value=9]
[][],
java.lang.Integer[value=16][][],
java.lang.Integer[value=25][]
[],null,null,null,null,null},size=5][modCount=5][][]

您可以使用此通用toString方法来实现您自己类的toString方法,如下所示:

public String toString()
{
	return new ObjectAnalyzer().toString(this);
}

这是一种无需麻烦且毫无疑问是有用的方法,可以提供通用的toString方法。但是,在您对不再需要实现toString感到过于兴奋之前,请记住,对内部构件进行不受控制的访问的日子已经屈指可数了。

清单5.15 objectAnalyzer/ObjectAnalyzerTest.java

package objectAnalyzer;

import java.util.*;

/**
 * This program uses reflection to spy on objects.
 * @version 1.13 2018-03-16
 * @author Cay Horstmann
 */
public class ObjectAnalyzerTest
{
   public static void main(String[] args)
         throws ReflectiveOperationException
   {
      var squares = new ArrayList<Integer>();
      for (int i = 1; i <= 5; i++)
         squares.add(i * i);
      System.out.println(new ObjectAnalyzer().toString(squares));
   }
}

清单5.16 objectAnalyzer/ObjectAnalyzer.java

package objectAnalyzer;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;

public class ObjectAnalyzer
{
   private ArrayList<Object> visited = new ArrayList<>();

   /**
    * Converts an object to a string representation that lists all fields.
    * @param obj an object
    * @return a string with the object's class name and all field names and values
    */
   public String toString(Object obj)
         throws ReflectiveOperationException
   {
      if (obj == null) return "null";
      if (visited.contains(obj)) return "...";
      visited.add(obj);
      Class cl = obj.getClass();
      if (cl == String.class) return (String) obj;
      if (cl.isArray())
      {
         String r = cl.getComponentType() + "[]{";
         for (int i = 0; i < Array.getLength(obj); i++)
         {
            if (i > 0) r += ",";
            Object val = Array.get(obj, i);
            if (cl.getComponentType().isPrimitive()) r += val;
            else r += toString(val);
         }
         return r + "}";
      }

      String r = cl.getName();
      // inspect the fields of this class and all superclasses
      do
      {
         r += "[";
         Field[] fields = cl.getDeclaredFields();
         AccessibleObject.setAccessible(fields, true);
         // get the names and values of all fields
         for (Field f : fields)
         {
            if (!Modifier.isStatic(f.getModifiers()))
            {
               if (!r.endsWith("[")) r += ",";
               r += f.getName() + "=";
               Class t = f.getType();
               Object val = f.get(obj);
               if (t.isPrimitive()) r += val;
               else r += toString(val);
            }
         }
         r += "]";
         cl = cl.getSuperclass();
      }
      while (cl != null);

      return r;
   }
}

java.lang.reflect.AccessibleObject 1.2

  • void setAccessible(boolean flag)
    设置或清除此可访问对象的可访问性标志,或者在访问被拒绝时引发IllegalAccessException。
  • void setAccessible(boolean flag)
  • boolean trySetAccessible() 9
    设置此可访问对象的可访问性标志,如果访问被拒绝,则返回false。
  • boolean isAccessible()
    获取此可访问对象的可访问性标志的值。
  • static void setAccessible(AccessibleObject[] array, boolean flag)
    是为对象数组设置可访问性标志的方便方法。

java.lang.Class 1.1

  • Field getField(String name)
  • Field[] getFields()
    获取具有给定名称的公共字段或所有字段的数组。
  • Field getDeclaredField(String name)
  • Field[] getDeclaredFields()
    获取在此类中声明的具有给定名称的字段或所有字段的数组。

java.lang.reflect.Field 1.1

  • Object get(Object obj)
    获取对象obj中此Field对象描述的字段的值。
  • void set(Object obj, Object newValue)
    将对象obj中此Field对象描述的字段设置为新值。

5.7.6 使用反射编写泛型数组代码

java.lang.reflect包中的Array类允许您动态创建数组。例如,在Array类中的copyOf方法的实现中就使用了这种方法。回想一下如何使用此方法来增大已满的数组。

var a = new Employee[100];
. . .
// array is full
a = Arrays.copyOf(a, 2 * a.length);

怎样才能写出这样一个通用的方法呢?它有助于将Employee[]数组转换为Object[]数组。听起来不错。这是第一次尝试:

public static Object[] badCopyOf(Object[] a, int newLength) // not u
{
    var newArray = new Object[newLength];
    System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength);
    return newArray;
}

但是,实际使用结果数组时存在问题。此代码返回的数组类型是对象数组(Object[]),因为我们使用代码行创建了数组

new Object[newLength]

不能将对象数组强制转换为雇员数组(Employee[])。虚拟机将在运行时生成ClassCastException。重点是,正如我们前面提到的,Java数组记住它的条目类型,即创建new表达式的元素类型。暂时将Employee[]强制转换为Object[]数组,然后将其强制转换回是合法的,但是以Object[]数组的形式开始其生命的数组永远不能强制转换为Employee[]数组。要编写这种通用数组代码,我们需要能够生成与原始数组类型相同的新数组。为此,我们需要java.lang.reflect包中数组类的方法。键是构造新数组的Array类的静态newInstance方法。必须为此方法提供条目的类型和所需的长度作为参数。

Object newArray = Array.newInstance(componentType, newLength);

要实际执行此操作,我们需要获取新数组的长度和组件类型。

我们通过调用Array.getLength(a)来获取长度。数组类的静态getLength方法返回数组的长度。要获取新数组的组件类型:

  1. 首先,获取类对象a。
  2. 确认它确实是一个数组。
  3. 使用Class类(仅为表示数组的类对象定义)的getComponentType方法查找数组的正确类型。

为什么getLength是Array的方法,而getComponentType是Class的方法?我们不知道——反射方法的分布有时似乎有点特别。

下面是代码:

public static Object goodCopyOf(Object a, int newLength)
{
    Class cl = a.getClass();
    if (!cl.isArray()) return null;
    Class componentType = cl.getComponentType();
    int length = Array.getLength(a);
    Object newArray = Array.newInstance(componentType, newLength);
    System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
    return newArray;
}

注意,这个copyOf方法可以用于增长任何类型的数组,而不仅仅是对象数组。

int[] a = { 1, 2, 3, 4, 5 };
a = (int[]) goodCopyOf(a, 10);

为了实现这一点,goodCopyOf的参数被声明为Object类型,而不是Objects数组(Object[])。整数数组类型int[]可以转换为Object,但不能转换为对象数组!

清单5.17显示了这两种方法的作用。请注意,badcopyof返回值的强制转换将引发异常。

清单5.17 arrays/CopyOfTest.java

package arrays;

import java.lang.reflect.*;
import java.util.*;

/**
 * This program demonstrates the use of reflection for manipulating arrays.
 * @version 1.2 2012-05-04
 * @author Cay Horstmann
 */
public class CopyOfTest
{
   public static void main(String[] args)
   {
      int[] a = { 1, 2, 3 };
      a = (int[]) goodCopyOf(a, 10);
      System.out.println(Arrays.toString(a));

      String[] b = { "Tom", "Dick", "Harry" };
      b = (String[]) goodCopyOf(b, 10);
      System.out.println(Arrays.toString(b));

      System.out.println("The following call will generate an exception.");
      b = (String[]) badCopyOf(b, 10);
   }

   /**
    * This method attempts to grow an array by allocating a new array and copying all elements.
    * @param a the array to grow
    * @param newLength the new length
    * @return a larger array that contains all elements of a. However, the returned 
    * array has type Object[], not the same type as a
    */
   public static Object[] badCopyOf(Object[] a, int newLength) // not useful
   {
      var newArray = new Object[newLength];
      System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
      return newArray;
   }

   /**
    * This method grows an array by allocating a new array of the same type and
    * copying all elements.
    * @param a the array to grow. This can be an object array or a primitive
    * type array
    * @return a larger array that contains all elements of a.
    */
   public static Object goodCopyOf(Object a, int newLength) 
   {
      Class cl = a.getClass();
      if (!cl.isArray()) return null;
      Class componentType = cl.getComponentType();
      int length = Array.getLength(a);
      Object newArray = Array.newInstance(componentType, newLength);
      System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
      return newArray;
   }
}

java.lang.reflect.Array 1.1

  • static Object get(Object array, int index)
  • static xxx getXxx(Object array, int index)
    (xxx是基元类型boolean,byte,char,double,float,int,long,或者short。)这些方法返回给定数组的值,它存储在给定的索引中
  • static void set(Object array, int index, Object newValue)
  • static setXxx(Object array, int index, xxx newValue)
    (xxx是基元类型boolean,byte,char,double,float,int,long,或者short。)这些方法返回给定数组的值,它存储在给定的索引中
  • static int getLength(Object array)
    返回给定数组的长度
  • static Object newInstance(Class componentType, int length)
  • static Object newInstance(Class componentType, int[] lengths)
    返回具有给定维度的给定组件类型的新数组。

5.7.7 调用任意方法和构造函数

在C和C++中,可以通过函数指针执行任意函数。从表面上看,Java没有方法指针,即将方法的位置赋予另一种方法,因此第二种方法可以稍后调用它。事实上,Java的设计者已经说过方法指针是危险的和容易出错的,Java接口和lambda表达式(在下一章中讨论)是一个优越的解决方案。但是,反射机制允许您调用任意方法。

回想一下,您可以使用Field类的get方法检查对象的字段。类似地,Method类有一个invoke方法,允许您调用包装在当前Method对象中的方法。invoke方法的签名是

Object invoke(Object obj, Object... args)

第一个参数是隐式参数,其余的对象提供显式参数。

对于静态方法,第一个参数被忽略,您可以将其设置为null。

例如,如果m1表示Employee类的getName方法,则以下代码显示如何调用它:

String n = (String) m1.invoke(harry);

如果返回类型是基元类型,则invoke方法将返回包装类型。例如,假设m2表示Employee类的getSalary方法。然后,返回的对象实际上是一个双精度对象,您必须相应地对其进行强制转换。使用自动拆箱将其转换为double:

double s = (Double) m2.invoke(harry); 

如何获取Method对象?当然,您可以调用getDeclaredMethods并搜索返回的Method对象数组,直到找到所需的方法为止。或者,可以调用Class类的getMethod方法。这类似于getField方法,该方法使用具有字段名称的字符串并返回Field对象。但是,可能有几个方法具有相同的名称,因此您需要小心,以获得正确的方法。因此,还必须提供所需方法的参数类型。getMethod的签名是

Method getMethod(String name, Class... parameterTypes)

例如,下面是如何获取Employee类的getName和raiseSalary方法的方法指针:

Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);

使用类似的方法调用任意构造函数。将构造函数的参数类型提供给Class.getConstructor方法,并将参数值提供给Constructor.newInstance方法:

Class cl = Random.class; // or any other class with a constructor th
// accepts a long parameter
Constructor cons = cl.getConstructor(long.class);
Object obj = cons.newInstance(42L);

既然您已经了解了使用Method对象的规则,那么让我们让它们开始工作。清单5.18是一个为数学函数(如Math.sqrt或Math.sin)打印值表的程序。打印输出如下:

当然,打印表格的代码独立于表格中的实际功能。

double dx = (to - from) / (n - 1);
for (double x = from; x <= to; x += dx)
{
    double y = (Double) f.invoke(null, x);
    System.out.printf("%10.4f | %10.4f%n", x, y);
}

这里,f是Method类型的对象。invoke的第一个参数为null,因为我们调用的是静态方法。

为了将Math.sqrt函数制成表格,我们将f设置为

Math.class.getMethod("sqrt", double.class)

这是名为sqrt、类型为double的单个参数的Math类的方法。

清单5.18显示了通用制表器的完整代码和一些测试运行。

清单5.18 methods/MethodTableTest.java

package methods;

import java.lang.reflect.*;

/**
 * This program shows how to invoke methods through reflection.
 * @version 1.2 2012-05-04
 * @author Cay Horstmann
 */
public class MethodTableTest
{
   public static void main(String[] args)
         throws ReflectiveOperationException
   {
      // get method pointers to the square and sqrt methods
      Method square = MethodTableTest.class.getMethod("square", double.class);
      Method sqrt = Math.class.getMethod("sqrt", double.class);

      // print tables of x- and y-values
      printTable(1, 10, 10, square);
      printTable(1, 10, 10, sqrt);
   }

   /**
    * Returns the square of a number
    * @param x a number
    * @return x squared
    */
   public static double square(double x)
   {
      return x * x;
   }

   /**
    * Prints a table with x- and y-values for a method
    * @param from the lower bound for the x-values
    * @param to the upper bound for the x-values
    * @param n the number of rows in the table
    * @param f a method with a double parameter and double return value
    */
   public static void printTable(double from, double to, int n, Method f)
         throws ReflectiveOperationException
   {
      // print out the method as table header
      System.out.println(f);

      double dx = (to - from) / (n - 1);

      for (double x = from; x <= to; x += dx)
      {
         double y = (Double) f.invoke(null, x);
         System.out.printf("%10.4f | %10.4f%n", x, y);
      }
   }
}

如本例所示,您可以对Method对象执行任何操作,这些操作可以对C中的函数指针(或C#中的委托)执行。就像在C中一样,这种类型的编程通常非常不方便,而且总是容易出错。如果使用错误参数调用方法,会发生什么情况?invoke方法引发异常。

此外,invoke的参数和返回值必须是object类型。那就意味着你必须经常来回走动。结果,编译器就失去了检查代码的机会,因此只有在测试过程中,错误才会显现出来,而此时查找和修复更为繁琐。此外,使用反射获取方法指针的代码比直接调用方法的代码慢得多。

因此,我们建议您仅在绝对必要时在自己的程序中使用Method对象。使用接口,和Java 8一样,lambda表达式(下一章的主题)几乎总是更好的主意。特别是,我们对Java开发人员进行了回应,建议不要使用回调函数的Method对象。为回调使用接口会导致代码运行更快,并且更易于维护。

java.lang.reflect.Method 1.1

  • public Object invoke(Object implicitParameter, Object[] explicitParameters)
    调用此对象描述的方法,传递给定参数并返回该方法返回的值。对于静态方法,将空值作为隐式参数传递。使用包装器传递基元类型值。基元类型返回值必须展开。

猜你喜欢

转载自blog.csdn.net/nbda1121440/article/details/90748751