Java17-教程-续-二-

116 阅读52分钟

Java17 教程·续(二)

原文:More Java 17

协议:CC BY-NC-SA 4.0

二、反射

在本章中,您将学习:

  • 什么是反射

  • 什么是类装入器以及关于内置类装入器

  • 如何使用反射获取类、构造函数、方法等信息?运行时

  • 如何使用反射访问对象和类的字段

  • 如何使用反射创建类的对象

  • 如何使用反射调用类的方法

  • 如何使用反射创建数组

本章中的大多数示例程序都是清单 2-1 中声明的jdojo.reflection module的成员。我在这一章中使用了更多的模块,我将在后面展示。

// module-info.java
module jdojo.reflection {
    exports com.jdojo.reflection;
}

Listing 2-1The Declaration of a jdojo.reflection Module

什么是反思?

反射是程序在执行过程中查询和修改其状态“作为数据”的能力。程序查询或获取自身信息的能力称为自省。程序在执行过程中修改其执行状态、修改其自身的解释或含义,或者向程序添加新行为的能力称为调解。反射进一步分为两类:

  • 结构反射

  • 行为反思

程序查询其数据和代码实现的能力称为结构自省,而修改或创建新的数据结构和代码的能力称为结构调解。

程序获取其运行时环境信息的能力称为行为自省,而修改运行时环境的能力称为行为调解。

为程序提供查询或修改其状态的能力需要一种将执行状态编码为数据的机制。换句话说,程序应该能够将其执行状态表示为数据元素(如 Java 等面向对象语言中的对象),以便可以查询和修改。将执行状态编码成数据的过程称为具体化。如果一种编程语言为程序提供了反射能力,那么它就被称为反射语言。

Java 中的反射

Java 对反射的支持大多局限于内省。它以非常有限的形式支持代祷。Java 提供的自省特性允许您在运行时获得关于对象的类信息。Java 还允许您在运行时获得关于字段、方法、修饰符和类的超类的信息。

Java 提供的调解特性允许您创建一个类的实例,直到运行时才知道它的名称,在这样的实例上调用方法,并获取/设置它的字段。然而,Java 不允许你在运行时改变数据结构。例如,您不能在运行时向对象添加新的字段或方法。一个对象的所有字段总是在程序启动时被确定。行为调解的例子是在运行时改变方法执行或在运行时向类添加新方法的能力。Java 不提供任何这些中介特性。也就是说,你不能在运行时改变一个类的方法代码来改变它的执行行为;也不能在运行时向类中添加新方法。

Java 通过为类及其方法、构造函数、字段等提供对象表示来提供具体化。运行时。在大多数情况下,Java 不支持泛型类型的具体化。Java 5 增加了对泛型类型的支持。参见第三章了解通用类型的更多细节。程序可以在具体化的对象上工作,以便获得关于运行时执行的信息。例如,你一直在使用java.lang.Class类的对象来获取关于一个对象的类的信息。类对象是对象的类的字节码的具体化。当您想要收集关于一个对象的类的信息时,您不必担心实例化该对象的类的字节码。相反,Java 将字节码具体化为Class类的对象。

Java 中的反射功能是通过反射 API 提供的。大多数反射 API 类和接口都在java.lang.reflect包中。Java 中反射的核心Class类在java.lang package中。表 2-1 中列出了反射中一些常用的类。

表 2-1

反射中常用的类

|

类别名

|

描述

| | --- | --- | | Class | 这个类的对象代表 JVM 中由类装入器装入的单个类。 | | Field | 这个类的对象代表一个类或一个接口的单个字段。由该对象表示的字段可以是静态字段或实例字段。 | | Constructor | 这个类的对象代表一个类的单个构造函数。 | | Method | 这个类的对象表示一个类的方法或一个接口。该对象表示的方法可以是类方法或实例方法。 | | Modifier | 该类具有静态方法,用于解码类及其成员的访问修饰符。 | | Parameter | 这个类的对象代表一个方法的参数。 | | Array | 该类提供了用于在运行时创建数组的静态方法。 |

使用 Java 中的反射特性可以做的一些事情如下:

  • 如果有对象引用,就可以确定对象的类名。

  • 如果你有一个类名,你可以知道它的完整描述,例如,它的包名,它的访问修饰符,等等。

  • 如果你有一个类名,你可以决定类中定义的方法,它们的返回类型,访问修饰符,参数类型,参数名,等等。Java 8 中增加了对参数名的支持。

  • 如果您有一个类名,您可以确定该类的所有字段描述。

  • 如果你有一个类名,你可以确定类中定义的所有构造函数。

  • 如果您有一个类名,您可以使用该类的一个构造函数来创建该类的对象。

  • 如果有一个对象引用,只要知道方法名和方法的参数类型,就可以调用它的方法。

  • 您可以在运行时获取或设置对象的状态。

  • 您可以在运行时动态创建一个类型的数组并操作其元素。

加载类

在 Java 中,Class<T>类是反射的核心。Class<T>类是一个泛型类。它接受一个类型参数,这是由Class对象表示的类的类型。例如,Class<String>表示String类的类对象。Class<?>表示未知类别的类别类型。

Class类让你在运行时发现关于一个类的一切。Class类的对象表示运行时程序中的一个类。当您在程序中创建一个对象时,Java 加载该类的字节码,并创建一个Class类的对象来表示字节码。Java 使用那个Class对象来创建该类的任何对象。无论在程序中创建多少个类对象,Java 都只为 JVM 中的类装入器从一个模块装入的每个类创建一个Class对象。模块中的每个类也只由一个特定的类装入器装入一次。在 JVM 中,一个类由它的完全限定名、它的类装入器和它的模块唯一地标识。如果两个不同的类装入器装入同一个类,这两个装入的类被认为是两个不同的类,它们的对象互不兼容。

您可以通过以下方式之一获取对某个类的类对象的引用:

  • 使用类文本

  • 使用Object类的getClass()方法

  • 使用Class类的forName()静态方法

使用类文本

类文字是类名或接口名,后跟一个点和单词“class”例如,如果你有一个类Test,它的类文字是Test.class,你可以写

Class<Test> testClass = Test.class;

请注意,类文本总是与类名一起使用,而不是与对象引用一起使用。以下获取类引用的语句无效:

Test t = new Test();
Class<Test> testClass = t.class;   // A compile-time error.
                                   // Must use Test.class

您还可以使用类文字获得原始数据类型的类对象和关键字 void,如boolean.classbyte.classchar.classshort.classint.classlong.classfloat.classdouble.classvoid.class。这些原始数据类型的每个包装器类都有一个名为TYPE的静态字段,该字段引用了它所代表的原始数据类型的类对象。所以int.classInteger.TYPE指的是同一个类对象,表达式int.class == IntegerTYPE评估为true。表 2-2 显示了所有原始数据类型的类文字和void关键字。

表 2-2

原始数据类型的类文字和void关键字

|

数据类型

|

原始类文字

|

包装类静态字段

| | --- | --- | --- | | boolean | boolean.class | Boolean.TYPE | | Byte | byte.class | Byte.TYPE | | Char | char.class | Character.TYPE | | Short | short.class | Short.TYPE | | Int | int.class | Integer.TYPE | | Long | long.class | Long.TYPE | | Float | float.class | Float.TYPE | | Double | double.class | Double.TYPE | | Void | void.class | Void.TYPE |

使用Object::getClass()方法

Object类包含一个getClass()方法,该方法返回对该对象的类的Class对象的引用。这个方法在 Java 中的每个类中都可用,因为 Java 中的每个类都显式或隐式地继承了Object类。该方法被声明为 final,因此任何子类都不能重写它。例如,如果您将testRef作为对类Test的对象的引用,您可以获得对Test类的Class对象的引用,如下所示:

Test testRef = new Test();
Class<?> testClass = testRef.getClass();

使用Class::forName()方法

Class类有一个forName()静态方法,它加载一个类并返回对其Class对象的引用。这是一个重载的方法。其声明如下:

  • Class<?> forName(String className) throws ClassNotFoundException

  • Class<?> forName(String className, boolean initialize, ClassLoader loader) throws ClassNotFoundException

  • Class<?> forName(Module module, String className)

forName(String className)方法接受要加载的类的完全限定名。它加载类,初始化它,并返回对它的Class对象的引用。如果该类已经被加载,它只是返回对该类的Class对象的引用。

forName(String className, boolean initialize, ClassLoader loader)方法为您提供了在加载类时初始化或不初始化类的选项,以及哪个类加载器应该加载类。如果类不能被加载,该方法的前两个版本抛出一个ClassNotFoundException

forName(Module module, String className)方法加载指定模块中具有指定类名的类,而不初始化加载的类。如果没有找到该类,该方法返回null

要加载名为pkg1.Test的类,您应该编写

Class testClass = Class.forName("pkg1.Test");

要使用forName()方法获得一个Class对象引用,在运行时之前不必知道类名。如果类还没有初始化,那么forName(String className方法会初始化这个类,而使用类的文字并不会初始化这个类。当一个类被初始化时,它所有的静态初始化器都被执行,所有的静态字段都被初始化。清单 2-2 列出了一个只有一个静态初始化器的Bulb类,它在控制台上打印一条消息。清单 2-3 使用各种方法来加载和初始化Bulb类。

// BulbTest.java
package com.jdojo.reflection;
public class BulbTest {
    public static void main(String[] args) {
        /* Uncomment only one of the following statements
           at a time. Observe the output to see the
           difference in the way the Bulb class is loaded
           and initialized.
         */
        BulbTest.createObject();
        // BulbTest.forNameVersion1();
        // BulbTest.forNameVersion2();
        // BulbTest.forNameVersion3();
        // BulbTest.classLiteral();
    }

    public static void classLiteral() {
        // Will load the class, but won't initialize it.
        Class<Bulb> c = Bulb.class;
    }
    public static void forNameVersion1() {
        try {
            String className = "com.jdojo.reflection.Bulb";
            // Will load and initialize the class
            Class c = Class.forName(className);
        } catch (ClassNotFoundException e) {
            System.out.println(e.getMessage());
        }
    }
    public static void forNameVersion2() {
        try {
            String className = "com.jdojo.reflection.Bulb";
            boolean initialize = false;
            // Get the classloader for the current class
            ClassLoader cLoader = BulbTest.class.
                getClassLoader();
            // Will load, but not initialize the class,
            // because we have set the initialize variable
            // to false
            Class c = Class.forName(className, initialize,
                cLoader);
        } catch (ClassNotFoundException e) {
            System.out.println(e.getMessage());
        }
    }
    public static void forNameVersion3() {
        String className = "com.jdojo.reflection.Bulb";
        // Get the module reference for the current class
        Module m = BulbTest.class.getModule();
        // Will load, but not initialize, the class
        Class c = Class.forName(m, className);
        if(c == null) {
            System.out.println(
                "The bulb class was not loaded.");
        } else {
            System.out.println(
                "The bulb class was loaded.");
        }
    }

    public static void createObject() {
        // Will load and initialize the Bulb class
        new Bulb();
    }
}
Loading class Bulb...

Listing 2-3Testing Class Loading and Initialization

// Bulb.java
package com.jdojo.reflection;
public class Bulb {
    static {
        // This will execute when this class is loaded
        // and initialized
        System.out.println("Loading class Bulb...");
    }
}

Listing 2-2A Bulb Class to Demonstrate Initialization of a Class

类装入器

在运行时,每个类型都由一个类加载器加载,它由一个java.lang. ClassLoader类的实例表示。你可以通过使用Class类的getClassLoader()方法来获取一个类型的类加载器的引用。下面的代码片段显示了如何获取Bulb类的类加载器:

Class<Bulb> cls = Bulb.class;
ClassLoader loader = cls.getClassLoader();

Java 运行时使用三个类加载器来加载类,如图 2-1 所示。箭头的方向表示委托方向。这些类装入器从不同的位置装入不同类型的类。您可以添加更多的类装入器,这将是ClassLoader类的一个子类。使用自定义类加载器,您可以从自定义位置加载类,对用户代码进行分区,以及卸载类。对于大多数应用程序,内置的类装入器就足够了。

img/323070_3_En_2_Fig1_HTML.jpg

图 2-1

类装入器层次结构

Note

从 JDK9 开始,应用程序类加载器可以委托给平台类加载器以及引导类加载器;平台类加载器可以委托给应用程序类加载器。

引导类加载器在库代码和虚拟机中实现。如果你调用getClassLoader(),在Object.class.getClassLoader() == null中,它托管的类返回null。并非所有的 Java SE 平台和 JDK 模块都由引导类加载器加载。举几个例子,由引导类加载器加载的模块有java.basejava.loggingjava.prefsjava.desktop。其他 Java SE 平台和 JDK 模块是由平台类加载器和应用程序类加载器加载的,这将在下面描述。使用-Xbootclasspath/a选项指定额外的引导类路径。其值存储在系统属性jdk.boot.class.path.append中。

平台类加载器可用于实现类加载扩展机制(不再支持用于加载类的 JDK8 扩展机制)。ClassLoader类包含一个名为 getPlatformClassLoader()的新静态方法,该方法返回平台类加载器的引用。表 2-3 列出了平台类加载器加载的模块。

表 2-3

平台类加载器加载的 JDK 模块

| `java.compiler` | `java.net` `.http` | `java.scripting` | | `java.security.jgss` | `java.smartcardio` | `java.sql` | | `java.sql.rowset` | `java.transaction.xa` | `java.xml.crypto` | | `jdk.accessibility` | `jdk.charsets` | `jdk.crypto.cryptoki` | | `jdk.crypto.ec` | `jdk.dynalink` | `jdk.httpserver` | | `jdk.jsobject` | `jdk.localedata` | `jdk.naming.dns` | | `jdk.security.auth` | `jdk.security.jgss` | `jdk.xml.dom` | | `jdk.zipfs` |   |   |

平台类加载器还有另一个用途。默认情况下,引导类装入器装入的类被授予所有权限。但是,有几个类不需要所有权限。这样的类由平台类加载器加载。

应用程序类加载器加载在模块路径上找到的应用程序模块和一些提供工具或导出工具 API 的 JDK 模块,如表 2-4 所列。您仍然可以使用ClassLoader类的名为 getSystemClassLoader()的静态方法来获取应用程序类加载器的引用。

表 2-4

由应用程序类加载器加载的 JDK 模块

| `jdk.compiler` | `jdk.internal.opt` | `jdk.jartool` | | `jdk.javadoc` | `jdk.jdeps` | `jdk.jlink` | | `jdk.unsupported.desktop` |   |   |

Note

在 JDK9 之前,扩展类加载器和应用类加载器都是java.net .URLClassLoader类的实例。在 JDK9 和更高版本中,平台类加载器(以前的扩展类加载器)和应用程序类加载器是内部 JDK 类的一个实例。如果您的代码依赖于特定于URLClassLoader类的方法,那么您的 JDK9 之前的代码可能会在 JDK9 或更高版本中中断。

表 2-3 和 2-4 中未列出的 JDK 模块由引导类加载器加载。清单 2-4 展示了如何打印模块名和它们的类装入器名。显示了部分输出。输出取决于运行时解析的模块。要打印所有的 JDK 模块及其类装入器,您应该在运行这个类之前在模块声明中添加一个“requires java.se.ee”。我将在第七章讨论模块层。

// ModulesByClassLoader.java
package com.jdojo.reflection;
public class ModulesByClassLoader {
    public static void main(String[] args) {
        // Get the boot layer
        ModuleLayer layer = ModuleLayer.boot();
        // Print all module's names and their class loader
        // names in the boot layer
        for (Module m : layer.modules()) {
            ClassLoader loader = m.getClassLoader();
            String moduleName = m.getName();
            String loaderName = loader == null ?
                "bootstrap" : loader.getName();
            System.out.printf("%s: %s%n", loaderName,
                moduleName);
        }
    }
}
bootstrap: java.base
platform: java.net.http
bootstrap: java.security.sasl
app: jdk.internal.opt
...

Listing 2-4Listing the Names of Loaded Modules by Class Loader

三个内置的类装入器协同工作来装入类。当应用程序类装入器需要装入一个类时,它搜索为所有类装入器定义的模块。如果为这些类装入器之一定义了合适的模块,则该类装入器装入该类,这意味着应用程序类装入器现在可以委托给引导程序类装入器和平台类装入器。如果在为这些类装入器定义的命名模块中没有找到某个类,应用程序类装入器将委托给它的父类,即平台类装入器。如果类仍然没有被加载,应用程序类加载器搜索类路径。如果在类路径上找到该类,它将该类作为其未命名模块的成员加载。如果在类路径上没有找到类,就会抛出一个ClassNotFoundException

当平台类装入器需要装入一个类时,它搜索为所有类装入器定义的模块。如果为这些类加载器之一定义了合适的模块,则该类加载器加载该类,这意味着平台类加载器可以委托给引导类加载器以及应用程序类加载器。如果在为这些类装入器定义的命名模块中没有找到某个类,那么平台类装入器将委托给它的父类,即引导类装入器。

当引导类装入器需要装入一个类时,它会搜索自己的命名模块列表。如果没有找到某个类,它会搜索通过命令行选项指定的文件和目录列表:Xbootclasspath/a。如果它在 bootstrap 类路径上找到一个类,它会将该类作为其未命名模块的成员加载。如果仍然没有找到一个类,那么抛出一个ClassNotFoundException

反思班级

本节演示了 Java 反射的特性,这些特性使您能够获得类的描述,比如它的包名、访问修饰符等。您将使用一个Person类,如清单 2-5 中所列,来演示反射特性。这是一个简单的类,有两个实例字段、两个构造函数和一些方法。它实现了两个接口。

// Person.java
package com.jdojo.reflection;
import java.io.Serializable;
public class Person implements Cloneable, Serializable {
    private int id = -1;
    private String name = "Unknown";
    public Person() {
    }
    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
    public int getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public Person clone() {
        try {
            return (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e.getMessage());
        }
    }
    @Override
    public String toString() {
        return "Person: id=" + this.id + ", name=" +
             this.name;
    }
}

Listing 2-5A Person Class Used to Demonstrate Reflection

清单 2-6 展示了如何获得一个类的描述。它列出了类访问修饰符、类名、超类名以及该类实现的所有接口。

// ClassReflection.java
package com.jdojo.reflection;
import java.lang.reflect.Modifier;
import java.lang.reflect.TypeVariable;
public class ClassReflection {
    public static void main(String[] args) {
        // Print the declaration of the Person class
        String clsDecl = getClassDescription(Person.class);
        System.out.println(clsDecl);
        // Print the declaration of the Class class
        clsDecl = getClassDescription(Class.class);
        System.out.println(clsDecl);
        // Print the declaration of the Runnable interface
        clsDecl = getClassDescription(Runnable.class);
        System.out.println(clsDecl);
        // Print the declaration of the class representing
        // the int data type
        clsDecl = getClassDescription(int.class);
        System.out.println(clsDecl);
    }
    public static
    String getClassDescription(Class<?> cls) {
        StringBuilder classDesc = new StringBuilder();
        // Prepare the modifiers and construct keyword
        // (class, enum, interface etc.)
        int modifierBits = 0;
        String keyword = " ";

        // Add keyword @interface, interface or class
        if (cls.isPrimitive()) {
            // We do not want to add anything
        } else if (cls.isInterface()) {
            modifierBits = cls.getModifiers() & Modifier.
                interfaceModifiers();
            // An annotation is an interface
            if (cls.isAnnotation()) {
                keyword = "@interface";
            } else {
                keyword = "interface";
            }
        } else if (cls.isEnum()) {
            modifierBits = cls.getModifiers() &
                Modifier.classModifiers();
            keyword = "enum";
        } else {
            modifierBits = cls.getModifiers() &
                Modifier.classModifiers();
            keyword = "class";
        }
        // Convert modifiers to their string representation
        String modifiers = Modifier.toString(modifierBits);
        // Append modifiers
        classDesc.append(modifiers);
        // Append the construct keyword
        classDesc.append(" ");
        classDesc.append(keyword);
        // Append simple name
        String simpleName = cls.getSimpleName();
        classDesc.append(" ");
        classDesc.append(simpleName);
        // Append generic parameters
        String genericParms = getGenericTypeParams(cls);
        classDesc.append(genericParms);
        // Append super class
        Class superClass = cls.getSuperclass();
        if (superClass != null) {
            String superClassSimpleName = superClass.
                getSimpleName();
            classDesc.append(" extends ");
            classDesc.append(superClassSimpleName);
        }
        // Append Interfaces
        String interfaces = ClassReflection.
            getClassInterfaces(cls);
        if (interfaces != null) {
            classDesc.append(" implements ");
            classDesc.append(interfaces);
        }
        return classDesc.toString().trim();
    }
    public static String getClassInterfaces(Class<?> cls) {
        // Get a comma-separated list of interfaces
        // implemented by the class
        Class<?>[] interfaces = cls.getInterfaces();
        if (interfaces.length == 0) {
            return null;
        }
        String[] names = new String[interfaces.length];
        for (int i = 0; i < interfaces.length; i++) {
            names[i] = interfaces[i].getSimpleName();
        }
        String interfacesList = String.join(", ", names);
        return interfacesList;
    }
    public static
    String getGenericTypeParams(Class<?> cls) {
        StringBuilder sb = new StringBuilder();
        TypeVariable<?>[] typeParms = cls.
            getTypeParameters();
        if (typeParms.length == 0) {
            return "";
        }
        String[] paramNames = new String[typeParms.
            length];
        for (int i = 0; i < typeParms.length; i++) {
            paramNames[i] = typeParms[i].getTypeName();
        }
        sb.append('<');
        String parmsList = String.join(",", paramNames);
        sb.append(parmsList);
        sb.append('>');
        return sb.toString();
    }
}

public class Person extends Object implements Cloneable,
     Serializable
public final class Class<T> extends Object implements
     Serializable, GenericDeclaration,
Type, AnnotatedElement
public abstract interface Runnable
int

Listing 2-6Reflecting on a Class

Class类的getName()方法返回该类的全限定名。要获得简单的类名,使用Class类的getSimpleName()方法,如下所示:

String simpleName = c.getSimpleName();

类的修饰符是在类声明中出现在关键字类之前的关键字。在下面的例子中,public 和 abstract 是MyClass类的修饰符:

public abstract class MyClass {
    // Code goes here
}

Class类的getModifiers()方法返回该类的所有修饰符。注意,getModifiers()方法返回一个整数。要获得修饰符的文本形式,您需要调用Modifier类的toString(int modifiers)静态方法,以整数形式传递修饰符值。假设cls是一个Class对象的引用,你得到这个类的修饰符,如下所示:

// You need to AND the returned value from the
// getModifiers() method with appropriate value returned
// from xxxModifiers() method of the Modifiers class
int mod = cls.getModifiers() & Modifier.classModifiers(); String modStr = Modifier.toString(mod);

获得一个类的超类的名字是很简单的。使用Class类的getSuperclass()方法获取超类的引用。注意,Java 中的每个类都有一个超类,除了Object类。如果在Object类上调用了getSuperclass()方法,它将返回null:

Class superClass = cls.getSuperclass();
if (superClass != null) {
    String superClassName = superClass.getSimpleName();
}

Note

Class类的getSuperclass()方法在表示Object类、接口类如List.class和基本类型类如int.classvoid.class等时返回null

要获得一个类实现的所有接口的名称,可以使用Class类的getInterfaces()方法。它返回一个Class对象的数组。数组中的每个元素表示由类实现的一个接口:

// Get all interfaces implemented by cls
Class<?>[] interfaces = cls.getInterfaces();

ClassReflection类的getClassDescription()方法将类声明的所有部分放入一个字符串并返回该字符串。这个类的main()方法演示了如何使用这个类。

Class类的一个名为toGenericString()的方法返回一个描述该类的字符串。该字符串包含该类的修饰符和类型参数。呼叫Person.class.toGenericString()将返回public class com.jdojo.reflection.Person

反思领域

类的字段由java.lang.reflect.Field类的对象表示。Class类中的以下四个方法可用于获取关于类字段的信息:

  • Field[] getFields()

  • Field[] getDeclaredFields()

  • Field getField(String name)

  • Field getDeclaredField(String name)

getFields()方法返回类或接口的所有可访问的公共字段。可访问的公共字段包括在类中声明的或从其超类继承的公共字段。getDeclaredFields()方法返回出现在类声明中的所有字段。它不包括继承的字段。另外两个方法,getField()getDeclaredField(),用于获取Field对象,如果您知道字段的名称。让我们考虑下面的类 A 和 B 以及一个接口IConstants的声明:

interface IConstants {
    int DAYS_IN_WEEK = 7;
}
class A implements IConstants {
    private int aPrivate;
    public int aPublic;
    protected int aProtected;
}
class B extends A {
    private int bPrivate;
    public int bPublic;
    protected int bProtected;
}

如果bClass是类 B 的Class对象的引用,表达式bClass.getFields()将返回以下三个可访问的公共字段:

  • public int B.bPublic

  • public int A.aPublic

  • public static final int IConstants.DAYS_IN_WEEK

bClass.getDeclaredFields()方法将返回在类 B 中声明的三个字段:

  • 私人 int B.bPrivate

  • 公共 int B.bPublic

  • 受保护的 int 受保护的

要获取一个类及其超类的所有字段,必须使用getSuperclass()方法获取超类的引用,并使用这些方法的组合。清单 2-7 展示了如何获取一个类的字段信息。注意,当您在Person类的Class对象上调用getFields()方法时,您不会得到任何东西,因为Person类不包含任何公共字段。

// FieldReflection.java
package com.jdojo.reflection;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
public class FieldReflection {
    public static void main(String[] args) {
        Class<Person> cls = Person.class;
        // Print declared fields
        ArrayList<String> fieldsDescription =
            getDeclaredFieldsList(cls);
        System.out.println("Declared Fields for " +
            cls.getName());
        for (String desc : fieldsDescription) {
            System.out.println(desc);
        }
        // Get the accessible public fields
        fieldsDescription = getFieldsList(cls);
        System.out.println("\nAccessible Fields for " +
            cls.getName());
        for (String desc : fieldsDescription) {
            System.out.println(desc);
        }
    }
    public static
    ArrayList<String> getFieldsList(Class c) {
        Field[] fields = c.getFields();
        ArrayList<String> fieldsList =
            getFieldsDescription(fields);
        return fieldsList;
    }
    public static
    ArrayList<String> getDeclaredFieldsList(Class c) {
        Field[] fields = c.getDeclaredFields();
        ArrayList<String> fieldsList =
            getFieldsDescription(fields);
        return fieldsList;
    }

    public static ArrayList<String>
    getFieldsDescription(Field[] fields) {
        ArrayList<String> fieldList = new ArrayList<>();
        for (Field f : fields) {
            // Get the modifiers for the field
            int mod = f.getModifiers() &
                Modifier.fieldModifiers();
            String modifiers = Modifier.toString(mod);
            // Get the simple name of the field type
            Class<?> type = f.getType();
            String typeName = type.getSimpleName();
            // Get the name of the field
            String fieldName = f.getName();
            fieldList.add(modifiers + " " + typeName +
                " " + fieldName);
        }
        return fieldList;
    }
}

Declared Fields for com.jdojo.reflection.Person
private int id
private String name
Accessible Fields for com.jdojo.reflection.Person

Listing 2-7Reflecting on Fields of a Class

Note

您不能使用这种技术来描述数组对象的长度字段。每种数组类型都有相应的类。当您试图使用getFields()方法获取数组类的字段时,您会得到一个长度为零的Field对象的数组。数组长度不是数组的类定义的一部分。相反,它作为数组对象的一部分存储在对象头中。

反映在可执行文件上

Method类的一个实例代表一个方法。Constructor类的一个实例代表一个构造函数。在结构上,方法和构造函数有一些共同点。两者都使用修饰符、参数和 throws 子句。两者都可以执行。这些类从一个公共的抽象超类Executable继承而来。两者通用的检索信息的方法是Executable类的方法。

Executable中的参数由Parameter类的对象表示。Executable类中的getParameters()方法返回一个Executable的所有参数作为Parameter[]。默认情况下,形参名称不存储在类文件中,以保持文件较小。Parameter类的getName()方法返回合成的参数名,如arg0arg1等。除非保留实际的参数名。如果您想在类文件中保留实际的参数名,您需要使用-parameters选项和javac编译器来编译源代码。

Executable类的getExceptionTypes()方法返回一个Class对象的数组,该数组描述了由Executable抛出的异常。如果在throws子句中没有列出异常,它将返回一个长度为零的数组。

Executable类的getModifiers()方法将修饰符作为int返回。

Executable类的getTypeParameters()方法返回一个代表泛型方法/构造函数的类型参数的TypeVariable数组。本章中的示例不包括方法/构造函数中的泛型类型变量声明。

清单 2-8 包含一个由静态方法组成的实用程序类,用于获取关于Executable的信息,比如修饰符、参数和异常的列表。在后面的章节中讨论方法和构造函数时,我会用到这个类。

// ExecutableUtil.java
package com.jdojo.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;

public class ExecutableUtil {
    public static
    ArrayList<String> getParameters(Executable exec) {
        Parameter[] parms = exec.getParameters();
        ArrayList<String> parmList = new ArrayList<>();
        for (int i = 0; i < parms.length; i++) {
            // Get modifiers, type, and name of the
            // parameter
            int mod = parms[i].getModifiers() &
                Modifier.parameterModifiers();
            String modifiers = Modifier.toString(mod);
            String parmType = parms[i].getType().
                getSimpleName();
            String parmName = parms[i].getName();
            String temp = modifiers + " " + parmType +
                " " + parmName;
            // Trim it as it may have leading spaces when
            // modifiers are absent
            parmList.add(temp.trim());
        }

        return parmList;
    }
    public static
    ArrayList<String> getExceptionList(Executable exec) {
        ArrayList<String> exceptionList =
            new ArrayList<>();
        for (Class<?> c : exec.getExceptionTypes()) {
            exceptionList.add(c.getSimpleName());
        }

        return exceptionList;
    }
    public static String
    getThrowsClause(Executable exec) {
        ArrayList<String> exceptionList =
            getExceptionList(exec);
        String exceptions = ExecutableUtil.
            arrayListToString(exceptionList, ",");
        String throwsClause = "";

        if (exceptionList.size() > 0) {
            throwsClause = "throws " + exceptions;
        }
        return throwsClause;
    }
    public static String getModifiers(Executable exec) {
        // Get the modifiers for the class
        int mod = exec.getModifiers();
        if (exec instanceof Method) {
            mod = mod & Modifier.methodModifiers();
        } else if (exec instanceof Constructor) {
            mod = mod & Modifier.constructorModifiers();
        }
        return Modifier.toString(mod);
    }
    public static String
    arrayListToString(ArrayList<String> list,
                      String saparator) {
        String[] tempArray = new String[list.size()];
        tempArray = list.toArray(tempArray);
        String str = String.join(saparator, tempArray);
        return str;
    }
}

Listing 2-8A Utility Class to Get Information for an Executable

反思方法

Class类中的以下四个方法可用于获取关于一个类的方法的信息:

  • Method[] getMethods()

  • Method[] getDeclaredMethods()

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

  • Method getDeclaredMethod(String name, Class... parameterTypes)

getMethods()方法返回该类所有可访问的公共方法。可访问的公共方法包括在类中声明的或从超类继承的任何公共方法。getDeclaredMethods()方法返回所有只在类中声明的方法。它不返回任何从超类继承的方法。另外两个方法,getMethod()getDeclaredMethod(),用于获取Method对象,前提是您知道方法的名称及其参数类型。

Method类的getReturnType()方法返回Class对象,该对象包含关于方法返回类型的信息。

清单 2-9 展示了如何获取关于一个类的方法的信息。您可以取消注释main()方法中的代码,以打印 Person 类中的所有方法——在Person类中声明并从Object类继承。

// MethodReflection.java
package com.jdojo.reflection;
import java.lang.reflect.Method;
import java.util.ArrayList;
public class MethodReflection {
    public static void main(String[] args) {
        Class<Person> cls = Person.class;
        // Get the declared methods
        ArrayList<String> methodsDescription =
            getDeclaredMethodsList(cls);
        System.out.println("Declared Methods for " +
            cls.getName());
        for (String desc : methodsDescription) {
            System.out.println(desc);
        }
        /* Uncomment the following code to print all
           methods in the Person class
        // Get the accessible public methods
        methodsDescription = getMethodsList(cls);
        System.out.println("\nMethods for " + cls.getName());
        for (String desc : methodsDescription) {
            System.out.println(desc);
        }
         */
    }
    public static ArrayList<String>
    getMethodsList(Class c) {
        Method[] methods = c.getMethods();
        ArrayList<String> methodsList =
            getMethodsDescription(methods);
        return methodsList;
    }
    public static ArrayList<String>
    getDeclaredMethodsList(Class c) {
        Method[] methods = c.getDeclaredMethods();
        ArrayList<String> methodsList =
            getMethodsDescription(methods);
        return methodsList;
    }
    public static ArrayList<String>
    getMethodsDescription(Method[] methods) {
        ArrayList<String> methodList = new ArrayList<>();

        for (Method m : methods) {
            String modifiers = ExecutableUtil.
                getModifiers(m);
            // Get the method return type
            Class returnType = m.getReturnType();
            String returnTypeName =
                returnType.getSimpleName();
            // Get the name of the method
            String methodName = m.getName();
            // Get the parameters of the method
            ArrayList<String> paramsList =
                ExecutableUtil.getParameters(m);
            String params = ExecutableUtil.
                arrayListToString(paramsList, ",");
            // Get the Exceptions thrown by method
            String throwsClause = ExecutableUtil.
                getThrowsClause(m);
            methodList.add(modifiers + " " +
                returnTypeName + " " + methodName +
                    "(" + params + ") " + throwsClause);
        }
        return methodList;
    }
}

Declared Methods for com.jdojo.reflection.Person
public String toString()
public Object clone()
public String getName()
public int getId()
public void setName(String arg0)

Listing 2-9Reflecting on Methods of a Class

反思构造函数

获取关于类的构造函数的信息类似于获取关于类的方法的信息。Class类中的以下四个方法用于获取关于由Class对象表示的构造函数的信息:

  • Constructor[] getConstructors()

  • Constructor[] getDeclaredConstructors()

  • Constructor<T> getConstructor(Class... parameterTypes)

  • Constructor<T> getDeclaredConstructor(Class... parameterTypes)

getConstructors()方法返回所有公共构造函数。getDeclaredConstructors()方法返回所有声明的构造函数。另外两个方法,getConstructor()getDeclaredConstructor(),用于获取Constructor对象,如果你知道构造函数的参数类型。清单 2-10 展示了如何获取由Class对象表示的构造函数的信息。

// ConstructorReflection.java
package com.jdojo.reflection;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
public class ConstructorReflection {
    public static void main(String[] args) {
        Class<Person> cls = Person.class;
        // Get the declared constructors
        System.out.println("Constructors for " +
            cls.getName());
        Constructor[] constructors = cls.getConstructors();
        ArrayList<String> constructDescList =
            getConstructorsDescription(constructors);
        for (String desc : constructDescList) {
            System.out.println(desc);
        }
    }
    public static
    ArrayList<String> getConstructorsDescription(
            Constructor[] constructors) {
        ArrayList<String> constructorList =
            new ArrayList<>();
        for (Constructor constructor : constructors) {
            String modifiers = ExecutableUtil.
                getModifiers(constructor);
            // Get the name of the constructor
            String constructorName = constructor.getName();
            // Get the parameters of the constructor
            ArrayList<String> paramsList = ExecutableUtil.
                getParameters(constructor);
            String params = ExecutableUtil.
                arrayListToString(paramsList, ",");
            // Get the Exceptions thrown by the constructor
            String throwsClause = ExecutableUtil.
                getThrowsClause(constructor);
            constructorList.add(modifiers + " " +
                constructorName + "(" + params + ") " +
                throwsClause);
        }
        return constructorList;
    }
}

Constructors for com.jdojo.reflection.Person
public com.jdojo.reflection.Person()
public com.jdojo.reflection.Person(int arg0,String arg1)

Listing 2-10Reflecting on Constructors of a Class

创建对象

Java 允许你使用反射来创建一个类的对象。运行时才需要知道类名。您可以通过使用反射调用类的构造函数之一来创建对象。您还可以访问对象字段的值,设置它们的值,并调用它们的方法。如果您知道类名,并且在编译时可以访问该类代码,请不要使用反射来创建其对象;相反,在代码中使用 new 运算符来创建类的对象。通常,框架和库使用反射来创建对象。

您可以使用反射创建一个类的对象。在创建对象之前,需要获取构造函数的引用。上一节向您展示了如何获取一个类的特定构造函数的引用。使用Constructor类的newInstance()方法创建一个对象。您可以将实际参数传递给newInstance()方法的构造函数,声明如下:

public T newInstance(Object... initargs) throws
  InstantiationException,
  IllegalAccessException,
  IllegalArgumentException,
  InvocationTargetException

这里,initargs是构造函数的实际参数。您不会为无参数构造函数传递任何参数。

下面的代码片段获取对Person类的无参数构造函数的引用并调用它。为了简洁起见,我省略了异常处理:

Class<Person> cls = Person.class;
// Get the reference of the Person() constructor
Constructor<Person> noArgsCons = cls.getConstructor();
Person p = noArgsCons.newInstance();

清单 2-11 包含了完整的代码来说明如何使用Person类的Person(int, String)构造函数来创建一个使用反射的Person对象。注意Constructor<T>类是一个泛型类型。它的类型参数是声明构造函数的类类型,例如,Constructor<Person>类型代表Person类的一个构造函数。

// InvokeConstructorTest.java
package com.jdojo.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class InvokeConstructorTest {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;

        try {
            // Get the constructor "Person(int, String)"
            Constructor<Person> cons = personClass.
                getConstructor(int.class, String.class);
            // Invoke the constructor with values for id
            // and name
            Person chris = cons.newInstance(1994, "Chris");
            System.out.println(chris);
        } catch (NoSuchMethodException | SecurityException
                | InstantiationException
                | IllegalAccessException
                | IllegalArgumentException
                | InvocationTargetException e) {
            System.out.println(e.getMessage());
        }
    }
}

Person: id=1994, name=Chris

Listing 2-11Using a Specific Constructor to Create a New Object

调用方法

您可以使用反射调用对象的方法。您需要获取对想要调用的方法的引用。假设您想要调用Person类的setName()方法。您可以获取对 setName()方法的引用,如下所示:

Class<Person> personClass = Person.class;
Method setName = personClass.getMethod("setName",
    String.class);

要调用这个方法,在方法的引用上调用invoke()方法,声明如下:

public Object invoke(Object obj, Object... args)
    throws IllegalAccessException,
           lllegalArgumentException,
           InvocationTargetException

invoke()方法的第一个参数是您想要调用该方法的对象。如果Method对象代表一个静态方法,第一个参数被忽略或者可能是null。第二个参数是一个 varargs 参数,在该参数中,您按照方法声明中声明的顺序传递所有实际参数。

由于Person类的setName()方法接受一个String参数,您需要将一个String对象作为第二个参数传递给invoke()方法。清单 2-12 展示了如何使用反射调用Person对象上的方法。

// InvokeMethodTest.java
package com.jdojo.reflection;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class InvokeMethodTest {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;
        try {
            // Create an object of Person class
            Person p = personClass.newInstance();
            // Print the details of the Person object
            System.out.println(p);
            // Get the reference of the setName() method
            Method setName = personClass.getMethod(
                "setName", String.class);
            // Invoke the setName() method on p passing
            // passing "Ann" as the actual parameter
            setName.invoke(p, "Ann");
            // Print the details of the Person object
            System.out.println(p);
        } catch (InstantiationException
                | IllegalAccessException
                | NoSuchMethodException
                | SecurityException
                | IllegalArgumentException
                | InvocationTargetException e) {
            System.out.println(e.getMessage());
        }
    }
}

Person: id=-1, name=Unknown
Person: id=-1, name=Ann

Listing 2-12Invoking a Method on an Object Reference Using Reflection

访问字段

您可以使用反射读取或设置对象的字段值。首先,您需要获得您想要使用的字段的引用。要读取字段的值,需要对字段调用getXxx()方法,其中Xxx是字段的数据类型。例如,要读取一个布尔字段值,可以调用getBoolean()方法,要读取一个整型字段,可以调用getInt()方法。要设置字段的值,可以调用相应的setXxx()方法。下面是getInt()setInt()方法的声明,其中第一个参数 obj 是对象的引用,其字段正在被读取或写入:

  • int getint(object obj)throws illegal arguments exception,illegalaccessexception

  • 请参阅 setnt(object obj,int new value)throws illegal arguments exception,illegalaccessexception

Note

静态和实例字段的访问方式相同。在静态字段的情况下,get()set()方法的第一个参数是类/接口的引用。

请注意,您只能访问已声明为可访问的字段,如公共字段。在Person类中,所有字段都被声明为私有。因此,您不能使用普通的 Java 编程语言规则来访问这些字段。要访问通常不可访问的字段,例如,如果它被声明为 private,请参阅本章后面的“深度反射”一节。您将使用清单 2-13 中列出的PublicPerson类来学习访问字段的技术。

// PublicPerson.java
package com.jdojo.reflection;
public class PublicPerson {
    private int id = -1;
    public String name = "Unknown";
    public PublicPerson() {
    }
    @Override
    public String toString() {
        return "Person: id=" + this.id + ", name=" +
            this.name;
    }
}

Listing 2-13A PublicPerson Class with a Public Name Field

清单 2-14 演示了如何获取一个对象的字段的引用,以及如何读取和设置它的值。

// FieldAccessTest.java
package com.jdojo.reflection;
import java.lang.reflect.Field;
public class FieldAccessTest {
    public static void main(String[] args) {
        Class<PublicPerson> ppClass = PublicPerson.class;
        try {
            // Create an object of the PublicPerson class
            PublicPerson p = ppClass.newInstance();
            // Get the reference of the name field
            Field name = ppClass.getField("name");
            // Get and print the current value of the
            // name field
            String nameValue = (String) name.get(p);
            System.out.println("Current name is " +
                nameValue);

            // Set the value of name to Ann
            name.set(p, "Ann");
            // Get and print the new value of name field
            nameValue = (String) name.get(p);
            System.out.println("New name is " + nameValue);
        } catch (InstantiationException
                | IllegalAccessException
                | NoSuchFieldException
                | SecurityException
                | IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

Current name is Unknown
New name is Ann

Listing 2-14Accessing Fields Using Reflection

深刻的反思

使用反射可以做两件事:

  • 描述一个实体

  • 访问实体的成员

描述一个实体意味着了解实体的细节。例如,描述一个类意味着知道它的名字、修饰符、包、模块、字段、方法和构造函数。访问实体的成员意味着读写字段和调用方法和构造函数。描述一个实体不会引起任何访问控制的问题。如果您可以访问一个类文件,那么您应该能够知道该类文件中表示的实体的详细信息。然而,对实体成员的访问是由 Java 语言访问控制来控制的。例如,如果将某个类的某个字段声明为 private,则该字段应该只能在该类中访问。该类之外的代码应该不能访问该类的私有字段。然而,这是对的一半。静态访问成员时会应用 Java 语言访问控制规则。当您使用反射访问成员时,可以取消访问控制规则。

下面的代码片段访问了Person类的私有名称字段。这段代码将只在Person类中编译:

Person john = new Person();
String name = john.name;  // Accessing the private field
                          // name statically

Java 已经允许使用反射访问相当难访问的成员,比如类外部的私有字段。这叫深刻反思。对不可访问成员的反射访问使得在 Java 中拥有许多优秀的框架成为可能,比如 Hibernate 和 Spring。这些框架使用深度反射来完成大部分工作。您可以使用深度反射在Person类之外访问Person类的私有名称字段。

到目前为止,在这一章中,我保持了例子的简单性,并且没有违反 Java 语言访问控制。我只访问了公共字段、方法和构造函数;被访问的成员和访问代码在同一个模块中。在 JDK9 之前,访问不可访问的成员很容易。您所要做的就是在访问不可访问的FieldMethodConstructor对象之前调用setAccessible(true)方法。JDK9 中模块系统的引入让深度反思变得有点复杂。在本节及其子节中,我将带您浏览 JDK9 和更高版本中用于深度反射的规则和示例。

Note

如果存在安全管理器,执行深度反射的代码必须拥有ReflectPermission("suppressAccessChecks")权限。

要执行深度反射,需要使用Class对象的getDeclaredXxx()方法获取所需字段、方法和构造函数的引用,其中Xxx可以是FieldMethodConstructor。注意,使用getXxx()方法来获取不可访问的字段、方法或构造函数的引用将抛出一个IllegalAccessExceptionFieldMethodConstructor类将AccessibleObject类作为它们的超类。AccessibleObject类包含以下方法,允许您使用可访问标志:

  • void setAccessible(boolean flag)

  • static void setAccessible(AccessibleObject[] array, boolean flag)

  • boolean trySetAccessible()

  • boolean canAccess(Object obj)

setAccessible(boolean flag)方法将成员的可访问标志(FieldMethodConstructor)设置为truefalse。如果你试图访问一个不可访问的成员,你需要在访问成员之前调用成员对象上的setAccessible(true)。如果不能设置可访问标志,该方法抛出一个InaccessibleObjectException。静态setAccessible(AccessibleObject[] array, boolean flag)是为指定数组中的所有AccessibleObject设置可访问标志的方便方法。

trySetAccessible()方法试图在调用它的对象上将可访问标志设置为true。如果可访问标志被设置为true,则返回true,否则返回false。将此方法与setAccessible(true)方法进行比较。这个方法不会在失败时抛出运行时异常,而setAccessible(true)会。

如果调用者可以访问指定的obj对象的成员,则canAccess(Object obj)方法返回true。否则返回false。如果成员是静态成员或构造函数,obj 必须是null

我将在接下来的章节中讨论如何访问模块内、模块间、未命名模块中以及 JDK 模块中不可访问的成员。

模块内的深层反射

先说个例子。您想要访问一个Person对象的私有名称字段。首先,在一个Field对象中获取 name 字段的引用,并尝试读取它的当前值。清单 2-15 包含了IllegalAccess1类的代码。

// IllegalAccess1.java
package com.jdojo.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class IllegalAccess1 {
    public static void main(String[] args)
            throws Exception {
        // Get the class reference for the Person class
        String className = "com.jdojo.reflection.Person";
        Class<?> cls = Class.forName(className);
        // Create a Person object
        Constructor<?> cons = cls.getConstructor();
        Object person = cons.newInstance();
        // Get the reference of the name field
        Field nameField = cls.getDeclaredField("name");
        // Try accessing the name field by reading its
        // value
        String name = (String) nameField.get(person);
        // Print the person and its name separately
        System.out.println(person);
        System.out.println("name=" + name);
    }
}

Exception in thread "main" java.lang.
    IllegalAccessException: class com.jdojo.reflection.
IllegalAccess1 (in module jdojo.reflection) cannot access
    a member of class com.jdojo.
reflection.Person (in module jdojo.reflection) with
    modifiers "private"
       at java.base/jdk.internal.reflect.Reflection.
          newIllegalAccessException(Reflection.java:361)
       at java.base/java.lang.reflect.AccessibleObject.
          checkAccess(AccessibleObject.java:589)
       at java.base/java.lang.reflect.Field.checkAccess(
          Field.java:1075)
       at java.base/java.lang.reflect.Field.get(
          Field.java:416)
       at jdojo.reflection/com.jdojo.reflection.
          IllegalAccess1.main(IllegalAccess1.java:21)

Listing 2-15Accessing the Private Name Field of the Person Class

在清单 2-15 中,我在main()方法的throws子句中添加了Exception类,以保持方法内部的逻辑简单。我对本节中的所有示例都这样做,这样您就可以专注于非法访问规则,而不是异常处理。IllegalAccess1Person级在同一个jdojo.reflection模块中。您能够成功地创建一个Person对象,因为您使用了Person类的公共无参数构造函数。Person类中的 name 字段被声明为私有,从另一个类访问它失败。修复这个错误很简单——使用setAccessible(true)trySetAccessible()方法将accessible标志设置为Field对象。清单 2-16 包含完整的代码。

// IllegalAccess2.java
package com.jdojo.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class IllegalAccess2 {
    public static void main(String[] args)
            throws Exception {
        // Get the class reference for the Person class
        String className = "com.jdojo.reflection.Person";
        Class<?> cls = Class.forName(className);
        // Create a Person object
        Constructor<?> cons = cls.getConstructor();
        Object person = cons.newInstance();
        // Get the reference of the name field
        Field nameField = cls.getDeclaredField("name");
        // Try making the name field accessible before
        // accessing it
        boolean accessEnabled = nameField.
            trySetAccessible();
        if (accessEnabled) {
            // Try accessing the name field by reading
            // its value
            String name = (String) nameField.get(person);
            // Print the person and its name separately
            System.out.println(person);
            System.out.println("name=" + name);
        } else {
            System.out.println("The Person.name field " +
                "is not accessible.");
        }
    }
}

Person: id=-1, name=Unknown
name=Unknown

Listing 2-16Accessing the Private Name Field of the Person Class After Making It Accessible

到目前为止,一切看起来都很好。你可能认为如果你不能访问一个类的私有成员,你总是可以使用反射来访问它们。然而,事实并非总是如此。对类中不可访问成员的访问通过 Java 安全管理器来处理。默认情况下,在计算机上运行应用程序时,不会为应用程序安装安全管理器。您的应用程序缺少安全管理器,这使得您可以在将 accessible 标志设置为true之后,在同一个模块中访问一个类的所有字段、方法和构造函数,就像您在前面的示例中所做的那样。但是,如果为您的应用程序安装了安全管理器,您是否可以访问不可访问的类成员取决于授予您的应用程序访问此类成员的权限。您可以使用以下代码检查您的应用程序是否安装了安全管理器:

SecurityManager smgr = System.getSecurityManager();
if (smgr == null) {
    System.out.println(
        "Security manager is not installed.");
}

运行 Java 应用程序时,可以通过在命令行上传递- Djava.security.manager选项来安装默认的安全管理器。安全管理器使用 Java 安全策略文件来实现该策略文件中指定的规则。Java 安全策略文件是使用- Djava.security.policy命令行选项指定的。如果您想用 Java 安全管理器运行IllegalAccess2类,并将 Java 策略文件存储在C:\Java17LanguageFeatures\conf\myjava.policy文件中,您可以使用下面的命令:

C:\Java17LanguageFeatures>java -Djava.security.manager
-Djava.security.policy=conf\myjava.policy --module-path
    build\modules\jdojo.reflection
--module jdojo.reflection/com.jdojo.reflection.
    IllegalAccess2
Exception in thread "main" java.security.
    AccessControlException: access denied
("java.lang.reflect.ReflectPermission"
    "suppressAccessChecks")
        at java.base/java.security.AccessControlContext.
           checkPermission
           (AccessControlContext.java:472)
        at java.base/java.security.AccessController.
           checkPermission
           (AccessController.java:895)
        at java.base/java.lang.SecurityManager.
           checkPermission(SecurityManager.java:558)
        at java.base/java.lang.reflect.AccessibleObject.
           checkPermission
           (AccessibleObject.java:85)
        at java.base/java.lang.reflect.AccessibleObject.
           trySetAccessible
           (AccessibleObject.java:245)
        at jdojo.reflection/com.jdojo.reflection.
           IllegalAccess2.main
           (IllegalAccess2.java:26)

运行该命令时,myjava.policy文件为空,这意味着应用程序没有禁止 Java 语言访问控制的权限。

如果你想让你的程序使用反射来访问一个类的不可访问的字段,那么myjava.policy文件的内容应该如清单 2-17 所示。

grant {
    // Grant permission to all programs to access
    // inaccessible members
    permission java.lang.reflect.ReflectPermission
        "suppressAccessChecks";
};

Listing 2-17Contents of the conf\myjava.policy File

让我们用清单 2-17 所示的安全管理器和 Java 策略重新运行IllegalAccess2类:

C:\Java17LanguageFeatures>java -Djava.security.manager ^
-Djava.security.policy=conf\myjava.policy ^
--module-path build\modules\jdojo.reflection ^
--module ^
jdojo.reflection/com.jdojo.reflection.IllegalAccess2

Person: id=-1, name=Unknown
name=Unknown

这一次,当您授予适当的安全权限时,您能够访问Person类的私有名称字段。访问不可访问成员的规则才刚刚开始。当获得非法访问的代码和被非法访问的代码在同一个模块中时,您看到了在模块中进行深度反射的规则。下一节将描述跨模块的非法访问行为。

跨模块的深度反射

让我们建立一个名为jdojo.reflection.model的新模块,如清单 2-18 所示,以及其中一个名为Phone的简单类,如清单 2-19 所示。模块声明不包含模块语句。Phone类包含一个数字实例变量、两个构造函数、一个数字实例变量的 getter 和 setter。toString()方法返回电话号码。

// Phone.java
package com.jdojo.reflection.model;
public class Phone {
    private String number = "9999999999";

    public Phone() {
    }
    public Phone(String number) {
        this.number = number;
    }
    public String getNumber() {
        return number;
    }
    public void setNumber(String number) {
        this.number = number;
    }
    @Override
    public String toString() {
        return this.number;
    }
}

Listing 2-19A Phone Class

// module-info.java
module jdojo.reflection.model {
    // No module statements at this time
}

Listing 2-18The Declaration of a jdojo.reflection.model Module

让我们在jdojo.reflection模块中创建一个名为IllegalAccess3的类。该类将尝试在jdojo.reflection.model模块中创建一个Phone类的对象,并读取该对象的私有字段 number。清单 2-20 中的IllegalAccess3类包含完整的代码。和IllegalAccess2级很像。唯一的区别是,您跨越模块边界访问的是Phone类及其私有实例变量。

// IllegalAccess3.java
package com.jdojo.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class IllegalAccess3 {
    public static void main(String[] args)
            throws Exception {
        // Get the class reference for the Phone class
        String className =
            "com.jdojo.reflection.model.Phone";
        Class<?> cls = Class.forName(className);
        // Create a Phone object
        Constructor<?> cons = cls.getConstructor();
        Object phone = cons.newInstance();
        // Get the reference of the number field
        Field numberField = cls.getDeclaredField("number");
        // try making the number field accessible before
        // accessing it
        boolean accessEnabled = numberField.
            trySetAccessible();
        if (accessEnabled) {
            // Try accessing the number field by reading
            // its value
            String number = (String) numberField.
                get(phone);
            // Print the phone number
            System.out.println("number=" + number);
        } else {
            System.out.println("The Phone.number field " +
                "is not accessible.");
        }
    }
}

Listing 2-20Accessing the Private Number Field of the Phone Class

让我们使用以下命令运行IllegalAccess3类:

C:\Java17LanguageFeatures>java ^
--module-path build\modules\jdojo.reflection;build\modules\
    jdojo.reflection.model ^
--module ^
    jdojo.reflection/com.jdojo.reflection.IllegalAccess3
Exception in thread "main"
    java.lang.ClassNotFoundException:
    com.jdojo.reflection.model.Phone
        at java.base/jdk.internal.loader.
           BuiltinClassLoader.loadClass(BuiltinClassLoader.
           java:582)
        at java.base/jdk.internal.loader.ClassLoaders$
           AppClassLoader.loadClass(ClassLoaders.
           java:185)
        at java.base/java.lang.ClassLoader.loadClass
           (ClassLoader.java:496)
        at java.base/java.lang.Class.forName0
           (Native Method)
        at java.base/java.lang.Class.forName
           (Class.java:292)
        at jdoj9o.reflection/com.jdojo.reflection.
           IllegalAccess3.main(IllegalAccess3.java:11)

(“模块”后没有换行符和空格。)

你能猜出这个命令有什么问题吗?该错误表明运行时没有找到Phone类。您能够编译IllegalAccess3类,因为该类不使用源代码中的Phone类引用。它试图在运行时使用反射来使用Phone类。您已经在模块路径中包含了jdojo.reflection.model模块。但是,在模块路径中包含一个模块并不能解析该模块。jdojo.reflection模块不读取jdojo.reflection.model模块,所以运行IllegalAccess3不会解析jdojo.reflection.model模块,这就是运行时找不到Phone类的原因。您需要使用–addmodules命令行选项手动解析模块:

C:\Java17LanguageFeatures>java ^
--module-path build\modules\jdojo.reflection;build\modules\
    jdojo.reflection.model ^
--add-modules jdojo.reflection.model ^
--module ^
    jdojo.reflection/com.jdojo.reflection.IllegalAccess3
Exception in thread "main" java.lang.
    IllegalAccessException: class com.jdojo.reflection.
IllegalAccess3 (in module jdojo.reflection) cannot access
    class com.jdojo.reflection.model.Phone (in module
    jdojo.reflection.model) because module jdojo.
    reflection.model does not export com.jdojo.reflection.
    model to module jdojo.reflection
        at java.base/jdk.internal.reflect.Reflection.
           newIllegalAccessException
           (Reflection.java:361)
        at java.base/java.lang.reflect.AccessibleObject.
           checkAccess
           (AccessibleObject.java:589)
        at java.base/java.lang.reflect.Constructor.
           newInstance
           (Constructor.java:479)
        at jdojo.reflection/com.jdojo.reflection.
           IllegalAccess3.main
           (IllegalAccess3.java:15)

(“模块”后没有换行符和空格。)

这一次,运行时能够找到Phone类,但是它抱怨从另一个模块jdojo.reflection访问jdojo.reflection.model模块中的Phone类。该错误表明jdojo.reflection.model模块没有导出com.jdojo.reflection.model包,所以Phone类在com.jdojo.reflection.model包中,在jdojo.reflection.model模块之外是不可访问的。清单 2-21 包含了jdojo.reflection.model模块的修改版本。现在它导出了com.jdojo.reflection.model包。

// module-info.java
module jdojo.reflection.model {
    exports com.jdojo.reflection.model;
}

Listing 2-21The Modified Declaration of a jdojo.reflection.model Module

让我们使用前面的命令重新运行IllegalAccess3类:

C:\Java17LanguageFeatures>java ^
--module-path ^
    build\modules\jdojo.reflection;
    build\modules\jdojo.reflection.model
--add-modules jdojo.reflection.model ^
--module ^
    jdojo.reflection/com.jdojo.reflection.IllegalAccess3

The Phone.number field is not accessible.

(“反射”后没有换行符和空格。)

这一次,您能够实例化Phone类,但是您不能访问它的私有number字段。请注意,jdojo.reflection模块不读取jdojo.reflection.model模块。尽管如此,IllegalClass3类仍然能够访问Phone类,并使用反射对其进行实例化。如果您在IllegalAccess3类中编写以下代码片段,它将不会编译:

Phone phone = new Phone();

当模块 M 使用反射访问模块 N 中的类型时,从模块 M 到模块 N 的读取被隐式授予。当静态地需要这样的访问(没有反射)时,必须使用requires语句显式地指定这样的读取。这就是前面的命令在创建一个Phone类的对象时所做的事情。

如果您使用了IllegalAccess3类中的setAccessible(true)来使number字段可访问,那么前面的命令将会产生类似于下面的错误消息:

Exception in thread "main" java.lang.reflect.
InaccessibleObjectException: Unable to make field private
java.lang.String com.jdojo.reflection.model.Phone.number
accessible: module jdojo.reflection.model does not "opens
com.jdojo.reflection.model" to module jdojo.reflection
...

这个错误信息非常清晰。它表明运行时无法使私有的number字段可访问,因为jdojo.reflection.model模块没有向jdojo.reflection模块打开com.jdojo.reflection.model包。这里出现了打开一个模块的包和打开整个模块的概念。

导出模块的包会将对包中公共类型的访问权限以及这些类型的可访问公共成员授予另一个模块。导出包会在编译时和运行时授予访问权限。您可以使用反射来访问无需反射即可访问的相同可访问公共成员。也就是说,对于模块的导出包,总是强制执行 Java 语言访问控制。

如果您希望在运行时允许其他模块中的代码对一个模块中的包类型进行深度反射,您需要使用 opens 语句打开该模块的包。opens 语句的语法如下:

opens <package-name> [to <module-name>,<module-name>...];

该语法允许您向所有其他模块或一组特定模块打开一个包。在下面的声明中,模块M向模块ST打开它的包p:

module M {
    opens p to S, T;
}

在下面的声明中,模块N向所有其他模块开放其包q:

module N {
    opens q;
}

一个模块可能会导出并打开同一个包。如果其他模块需要在编译时和运行时静态访问包中的类型,并在运行时使用深度反射,则需要使用它。下面的模块声明将同一个包p导出并打开给所有其他模块:

module J {
    exports p;
    opens p;
}

模块声明中的一个opens语句允许你打开一个包给所有其他模块或选择性模块。如果您想向所有其他模块开放一个模块的所有包,您可以将该模块本身声明为开放模块。您可以通过在模块声明中使用 open 修饰符来声明一个开放的模块。下面声明了一个名为 K 的开放模块:

open module K {
    // Other module statements go here
}

打开的模块不能包含opens语句。这是因为一个开放的模块意味着它已经向所有其他模块开放了它的所有包以进行深度反射。模块 L 的以下声明是无效的,因为它将该模块声明为 open,同时包含一个opens语句:

open module L {
    opens p; // A compile-time error
    // Other module statements go here
}

可以在打开的模块中导出包。模块D的以下声明是有效的:

open module D {
    exports p;
    // Other module statements go here
}

所以,现在你知道如何处理jdojo.reflection.model模块,让jdojo.reflection模块对Phone类进行深度反射。您需要执行以下任一操作:

  • jdojo.reflection.model模块的com.jdojo.reflection.model包对所有其他模块打开,或者至少对 jdojo.reflection 模块打开。

  • jdojo.reflection.model模块声明为开放模块。

清单 2-22 和 2-23 包含了jdojo.reflection.model模块修改后的模块声明。您需要使用其中一个,而不是两个都用。对于这个例子,您不需要在模块的声明中导出包,因为您在编译时没有访问jdojo.reflection模块中的Phone类。

// module-info.java
open module jdojo.reflection.model {
    exports com.jdojo.reflection.model;
}

Listing 2-23The Modified Declaration of a model Module, Which Declares It As an Open Module

// module-info.java
module jdojo.reflection.model {
    exports com.jdojo.reflection.model;
    opens com.jdojo.reflection.model;
}

Listing 2-22The Modified Declaration of a model Module, Which Opens the com.jdojo.reflection.model Package to All Other Modules

让我们在打开com.jdojo.reflection.model包的情况下,使用前面的命令重新运行 IllegalAccess3 类。这一次,您将收到想要的输出:

C:\Java17LanguageFeatures>java ^
--module-path build\modules\jdojo.reflection;
    build\modules\jdojo.reflection.model ^
--add-modules jdojo.reflection.model ^
--module ^
    jdojo.reflection/com.jdojo.reflection.IllegalAccess3

number=9999999999

(“反射”后没有换行符和空格。)

深度反射和未命名模块

未命名模块中的所有包对所有其他模块都是开放的。因此,您总是可以在未命名模块中对类型执行深度反射。

对 JDK 模的深层思考

在 JDK9 之前,所有类型的成员 JDK9 内部成员和您的类型——都允许深度反射。JDK9 的主要目标之一是强大的封装,并且您不应该能够使用深度反射来访问对象的相当不可访问的成员。从 JDK9 开始,对 JDK 模块的深度反射只能在未命名的模块中进行。如果应用程序是模块化的,对 JDK 模块的深度反射是非法的。未命名模块的弱化限制只是为了向后兼容;现代应用程序不应该像私有字段一样访问 JDK 内部。

让我们看一个这样的例子。java.lang.Long类是不可变的。它包含一个名为value的私有字段,用于保存该对象表示的长整型值。清单 2-24 向您展示了如何使用深度反射访问和修改Long类的私有值字段,这在静态使用Long类时是不可能的。

// IllegalAccessJDKType.java
package com.jdojo.reflection;
import java.lang.reflect.Field;
public class IllegalAccessJDKType {
    public static void main(String[] args)
            throws Exception {
        // Create a Long object
        Long num = 1969L;
        System.out.println("#1: num = " + num);
        // Get the class reference for the Long class
        String className = "java.lang.Long";
        Class<?> cls = Class.forName(className);
        // Get the value field reference
        Field valueField = cls.getDeclaredField("value");
        // try making the value field accessible before
        // accessing it
        boolean accessEnabled = valueField.
            trySetAccessible();
        if (accessEnabled) {
            // Get and print the current value of the
            // Long.value private field of the num object
            // that you created in the beginning of this
            // method
            Long value = (Long) valueField.get(num);
            System.out.println("#2: num = " + value);
            // Change the value of the Long.value field
            valueField.set(num, 1968L);
            value = (Long) valueField.get(num);
            System.out.println("#3: num = " + value);
        } else {
            System.out.println("The Long.value field is " +
                "not accessible.");
        }
    }
}

Listing 2-24Accessing and Modifying the Private Value Field of the java.lang.Long Class Using Deep Reflection

在 main()方法的开头,您创建了一个名为numLong对象,并将其值设置为1969L:

Long num = 1969L;
System.out.println("#1: num = " + num);

稍后,您获取对Long类的Class对象的引用,并获取私有value字段的引用,并尝试使其可访问。如果您能够使字段可访问,那么您可以读取它的当前值,即 1969L。现在,您将它的值更改为 1968L,并在程序中读回它。

IllegalAccessJDKType类是jdojo.reflection模块的成员。让我们使用以下命令运行它:

C:\Java17LanguageFeatures>java ^
--module-path build\modules\jdojo.reflection ^
--module ^
jdojo.reflection/com.jdojo.reflection.IllegalAccessJDKType

#1: num = 1969
The Long.value field is not accessible.

您无法使Long类的私有value字段可访问,因为IllegalAccessJDKType类是命名模块的一部分,并且命名模块中的代码不允许非法访问 JDK 内部类型的成员。下面的命令从类路径重新运行该类(有效地取消对它的模块化,并隐式地使用未命名的模块),您将获得所需的输出。即使您已经访问私有字段三次,也要注意一次性警告:

C:\Java17LanguageFeatures>java ^
--class-path build\modules\jdojo.reflection ^
com.jdojo.reflection.IllegalAccessJDKType

#1: num = 1969
WARNING: An illegal reflective access operation has
    occurred
WARNING: Illegal reflective access by com.jdojo.reflection.
    IllegalAccessJDKType
(file:/C:/Java17LanguageFeatures/build/modules/
    jdojo.reflection/) to field java.lang.Long.value
WARNING: Please consider reporting this to the maintainers
of com.jdojo.reflection.IllegalAccessJDKType
WARNING: Use --illegal-access=warn to enable warnings of
    further illegal reflective access operations
WARNING: All illegal access operations will be denied in a
future release
#2: num = 1969
#3: num = 1968

在数组上反射

Java 提供了特殊的 API 来处理数组。Class类让你通过使用它的isArray()方法来发现Class引用是否代表一个数组。您还可以创建一个数组,并使用反射读取和修改其元素的值。java.lang.reflect.Array类用于动态创建一个数组并操作其元素。如前所述,您不能使用普通的反射过程来反射数组的length字段。然而,Array类提供了getLength()方法来获取数组的长度值。注意,Array类中的所有方法都是静态的,大多数方法都将第一个参数作为它们所操作的数组对象的引用。

要创建数组,请使用 array 类的newInstance()静态方法。方法是重载的,有两个版本:

  • Object newInstance(Class<?> componentType, int arrayLength)

  • Object newInstance(Class<?> componentType, int... dimensions)

该方法的一个版本创建指定组件类型和数组长度的数组。另一个版本创建指定组件类型和维度的数组。注意,newInstance()方法的返回类型是Object。您需要使用适当的强制转换将其转换为实际的数组类型。

如果你想创建一个长度为 5 的整型数组,你应该写

int[] ids = (int[]) Array.newInstance(int.class, 5);

该语句与下面的语句具有相同的效果:

int[] ids = new int[5];

如果你想创建一个 5x8 维的 int 数组,你应该写

int[][] matrix = (int[][]) Array.newInstance(
int.class, 5, 8);

清单 2-25 展示了如何动态创建一个数组并操作它的元素。

// ArrayReflection.java
package com.jdojo.reflection;
import java.lang.reflect.Array;
public class ArrayReflection {
    public static void main(String[] args) {
        try {
            // Create the array of int of length 2
            Object arrayObject = Array.newInstance(
                int.class, 2);
            // Print the values in array element. Default
            // values will be zero
            int n1 = Array.getInt(arrayObject, 0);
            int n2 = Array.getInt(arrayObject, 1);
            System.out.println("n1 = " + n1 +
                ", n2 = " + n2);
            // Set the values to both elements
            Array.set(arrayObject, 0, 101);
            Array.set(arrayObject, 1, 102);
            // Print the values in array element again
            n1 = Array.getInt(arrayObject, 0);
            n2 = Array.getInt(arrayObject, 1);
            System.out.println("n1 = " + n1 +
                ", n2 = " + n2);
        } catch (NegativeArraySizeException
                | IllegalArgumentException
                | ArrayIndexOutOfBoundsException e) {
            System.out.println(e.getMessage());
        }
    }
}

n1 = 0, n2 = 0
n1 = 101, n2 = 102

Listing 2-25Reflecting on Arrays

Java 不支持真正的多维数组。相反,它支持数组的数组。Class类包含一个名为getComponentType()的方法,该方法返回数组元素类型的Class对象。清单 2-26 展示了如何获得一个数组的维数。

// ArrayDimension.java
package com.jdojo.reflection;
public class ArrayDimension {
    public static void main(String[] args) {
        int[][][] intArray = new int[6][3][4];
        System.out.println("int[][][] dimension is " +
             getArrayDimension(intArray));
    }
    public static int getArrayDimension(Object array) {
        int dimension = 0;
        Class c = array.getClass();
        // Perform a check that the object is really
        // an array
        if (!c.isArray()) {
            throw new IllegalArgumentException(
                "Object is not an array.");
        }
        while (c.isArray()) {
            dimension++;
            c = c.getComponentType();
        }

        return dimension;
    }
}
int[][][] dimension is 3

Listing 2-26

Getting the Dimension of an Array

扩展数组

创建数组后,不能更改其长度。您可以创建一个更大的数组,并在运行时将旧数组元素复制到新数组中。Java 集合类如ArrayList应用了这种技术,让您可以向集合中添加元素,而不用担心它的长度。您可以使用Class类的getComponentType()方法和Array类的newInstance()方法的组合来创建一个给定类型的新数组。您可以使用System类的arraycopy()静态方法将旧数组元素复制到新数组中。清单 2-27 展示了如何使用反射创建一个特定类型的数组。为了清楚起见,所有运行时检查都被省略了。

// ExpandingArray.java
package com.jdojo.reflection;
import java.lang.reflect.Array;
import java.util.Arrays;
public class ExpandingArray {
    public static void main(String[] args) {
        // Create an array of length 2
        int[] ids = {101, 102};
        System.out.println("Old array length: " +
            ids.length);
        System.out.println("Old array elements: " +
            Arrays.toString(ids));
        // Expand the array by 1
        ids = (int[]) expandBy(ids, 1);
        // Set the third element to 103
        ids[2] = 103; // This is newly added element
        System.out.println("New array length: " +
            ids.length);
        System.out.println("New array elements: " +
            Arrays.toString(ids));
    }
    public static Object
    expandBy(Object oldArray, int increment) {
        // Get the length of old array using reflection
        int oldLength = Array.getLength(oldArray);
        int newLength = oldLength + increment;
        // Get the class of the old array
        Class<?> cls = oldArray.getClass();
        // Create a new array of the new length
        Object newArray = Array.newInstance(
            cls.getComponentType(), newLength);
        // Copy the old array elements to new array
        System.arraycopy(oldArray, 0, newArray,
            0, oldLength);
        return newArray;
    }
}

Old array length: 2
Old array elements: [101, 102]
New array length: 3
New array elements: [101, 102, 103]

Listing 2-27Expanding

an Array Using Reflection

谁应该使用反射?

如果您已经使用任何集成开发环境(IDE)开发了使用拖放功能的 GUI 应用程序,那么您已经使用了以某种形式使用反射的应用程序。所有允许您在设计时设置控件(比如按钮)属性的 GUI 工具都使用反射来获取该控件的属性列表。其他工具如类浏览器和调试器也使用反射。作为一名应用程序程序员,除非您正在开发使用反射 API 提供的动态性的高级应用程序,否则您不会经常使用反射。应该注意的是,使用过多的反射会降低应用程序的性能。

摘要

反射是程序在执行过程中“作为数据”查询和修改其状态的能力。Java 将类的字节码表示为Class类的对象,以方便反射。类字段、构造函数和方法可以分别作为FieldConstructorMethod类的对象来访问。使用一个Field对象,你可以访问和改变字段的值。使用一个Method对象,您可以调用该方法。使用一个Constructor对象,您可以调用一个类的给定构造函数。使用Array类,您还可以使用反射创建指定类型和维度的数组,并操作数组的元素。

Java 已经允许使用反射访问相当难访问的成员,比如类外部的私有字段。这叫深刻反思。在访问不可访问的成员之前,您需要调用该成员上的setAccessible(true),它可能是一个FieldMethodConstructor。如果可访问性不能被启用,setAccessible()方法抛出一个运行时异常。JDK9 出于同样的目的添加了一个trySetAccessible()方法,它不会抛出运行时异常。相反,如果可访问性被启用,它返回true,否则返回false

默认情况下,JDK9 和更高版本中禁止跨模块的深度反射。如果一个模块想要允许给定包中类型的深度反射,该模块必须至少向将使用深度反射的模块打开该包。您可以在模块声明中使用opens语句打开一个包。您可以将一个模块声明为开放模块,这将打开该模块中的所有包以进行深度反射。如果一个已命名的模块M使用反射来访问另一个模块N中的类型,那么模块M隐式地读取模块N。未命名模块中的所有包都打开进行深度反射。

JDK9 和更高版本只允许来自未命名模块或未模块化应用程序的代码对 JDK 内部类型进行深度反射。

练习

练习 1

什么是反思?

练习 2

说出两个包含反射相关类和接口的 Java 包。

运动 3

Class类的实例代表什么?

演习 4

列出三种获取类实例引用的方法。

锻炼 5

什么时候使用Class类的forName()方法来获取Class类的实例?

锻炼 6

说出三个内置的类装入器。你如何获得这些类装入器的引用?

锻炼 7

如果你得到一个Class类的引用,你怎么知道这个引用是否代表一个接口?

运动 8

FieldConstructorMethod类的实例代表什么?

演习 9

使用Class类的getFields()getDeclaredFields()方法有什么区别?

运动 10

您需要使用AccessibleObject类的setAccessible(true)trySetAccessible()方法来使FieldConstructorMethod对象可访问,即使它们是不可访问的(例如,它们被声明为私有)。这两种方法有什么区别?

演习 11

假设您有两个名为RS的模块。模块R包含一个带有公共方法m()的公共p.Test类。模块S中的代码需要使用类p.Test来声明变量并创建其对象。模块S也需要使用反射来访问模块Rp.Test类的公共方法m()。在声明模块R时,你至少需要做什么,模块S才能执行这些任务?

运动 12

什么是在模块中打开包?什么是开放模块?

运动 13

导出和打开一个模块的包有什么区别?举例说明何时需要导出并打开一个模块的同一个包。

运动 14

考虑名为jdojo.reflection.exercise.model的模块和该模块中的MagicNumber类的声明,如下所示:

// module-info.java
module jdojo.reflection.exercises.model {
    /* Add your module statements here */
}
// MagicNumber.java
package com.jdojo.reflection.exercises.model;
public class MagicNumber {
    private int number;
    public int getNumber() {
        return number;
    }
    public void setNumber(int number) {
        this.number = number;
    }
}

修改模块声明,以便其他模块中的代码可以对MagicNumber类的对象执行深度反射。在名为jdojo.reflection.exercises的模块中创建一个名为MagicNumberTest的类。MagicNumberTest类中的代码应该使用反射创建一个MagicNumber类的对象,直接设置它的private数字字段,并使用getNumber()方法读取number字段的当前值。

运动 15

在 Java 9 或更高版本中可以访问 JDK 类的私有成员吗?如果回答为是,请描述这种访问的规则和限制。

演习 16

假设有两个模块,PQ。模块P是一个开放模块。模块Q想要对模块P中的类型进行深度反射。模块Q需要读取其模块声明中的模块P吗?

演习 17

假设有两个模块,MN。模块M不向任何模块打开它的任何包,但是它向所有其他模块导出一个com.jdojo.m。模块N可以使用反射来访问模块Mcom.jdojo.m包的公共可访问成员吗?