Java 反射简介

此页面由 Dr Heinz M. KabutzUPL 许可下贡献

 

反射

今天早上从镜子里看回来的脸讲述了一个故事。我急需刮胡子(现在仍然需要)。昨天,当我坐在花园里时,我应该涂防晒霜,或者至少戴帽子。我尝试自己做培根,因为在克里特岛找不到这种美味佳肴,在韦伯烧烤炉上熏制了五个小时,我利用这段时间写了一篇关于并行流和虚拟线程的 Java Specialists Newsletter(第 311 期)。镜子里面的倒影眨了眨眼。盯着看的时间已经足够了,是时候开始写下我刚刚目睹的东西——反射。

Java 反射允许一个对象照镜子,发现它有哪些字段、方法和构造函数。我们可以读取和写入字段、调用方法,甚至通过调用构造函数创建新对象。就像我脸上的胡茬和轻微的晒伤一样,我们可以通过他人的眼睛看到自己。

为什么要关心阅读本教程?如果您已经了解反射,您可能想浏览一下以获得娱乐价值。但是,如果您从未听说过反射,那么现在是时候照照镜子,发现一种新的魔法,它可以让您用一些精心放置的反射诗歌来节省数千行代码。哦,我是否提到一些雇主发布了棘手的面试问题,这些问题可以通过反射轻松解决?我希望您会喜欢它,并尝试教程中的代码片段。感谢您与我一起踏上这段旅程。

 

Class

不,这不是打字错误。有一个名为 Class 的类。它是 Object 的子类。并且 Object 有一个 Class。一个不错的循环依赖关系。

我们如何获取对象的 Class?每个对象都有一个 getClass() 方法,它从 java.lang.Object 继承而来。当我们调用它时,我们会得到实际的实现 Class

例如,考虑以下代码。请注意,对于我们的代码片段,我们使用的是新的无名类,它们是 Java 21 的预览功能。请参阅 JEP 445。我们可以使用 java --enable-preview --source 21 GetClassDemo.java 直接运行它们。

// GetClassDemo.java
import java.util.List;
import java.util.ArrayList;

// Using new Unnamed Classes which is a preview feature of Java 21.
// See JEP 445 
void main() {
    List<String> list1 = new ArrayList<>();
    System.out.println(list1.getClass());
    var list2 = new ArrayList<String>();
    System.out.println(list2.getClass());
}

换句话说,无论变量如何声明,我们始终获得实际实现对象的类。我们如何获得 List 类?这很简单,使用类字面量。我们只需写下类的名称,后面跟着 .class,如下所示

// ClassLiteral.java
void main() {
    System.out.println(Number.class); // class java.lang.Number
    System.out.println(java.util.List.class); // interface java.util.List
}

我们还可以通过名称加载类作为 String,甚至不知道该类是否在运行时可用。例如,这里我们正在加载我们在 Console 上输入的任何类

// ClassForName.java
void main() throws ClassNotFoundException {
    var console = System.console();
    String className = console.readLine("Enter class name: ");
    System.out.println(Class.forName(className));
}

例如

heinz$ java --enable-preview --source 21 ClassForName.java 
Note: ClassForName.java uses preview features of Java SE 21.
Note: Recompile with -Xlint:preview for details.
Enter class name: java.util.Iterator
interface java.util.Iterator

每个类都加载到一个 ClassLoader 中。JDK 类都驻留在引导类加载器中,而我们的类则驻留在系统类加载器中,也称为应用程序类加载器。我们可以在这里看到类加载器

// ClassLoaderDemo.java
void main() {
    System.out.println(String.class.getClassLoader());
    System.out.println(this.getClass().getClassLoader());
}

有趣的是,根据我们调用此代码的方式,我们会得到不同的结果。例如,如果我们使用 java ClassLoaderDemo.java 调用它,那么类加载器的类型是 MemoryClassLoader,而如果我们先编译它,然后使用 java ClassLoaderDemo 调用它,那么它就是 AppClassLoader。JDK 类的类加载器返回 null

heinz$ java --enable-preview --source 21 ClassLoaderDemo.java 
null
com.sun.tools.javac.launcher.Main$MemoryClassLoader@6483f5ae

并且

heinz$ javac --enable-preview --source 21 ClassLoaderDemo.java
heinz$ java --enable-preview ClassLoaderDemo
null
jdk.internal.loader.ClassLoaders$AppClassLoader@3d71d552

类加载器的目的是出于安全原因对类进行分区。JDK 中的类根本无法看到我们的类,同样,AppClassLoader 中的类与 MemoryClassLoader 中的类没有任何关系。当我们编译类,然后也使用单文件命令 java SomeClass.java 启动它们时,这可能会导致一些意外情况。

 

浅层反射访问

一旦我们有了类,我们就可以发现很多关于它的信息,比如超类是谁,它有哪些公共成员,它实现了哪些接口。如果它是一个 sealed 类型,我们甚至可以找到子类型。

让我们尝试找到在 java.util.Iterator 上定义的方法

// MethodsOnIterator.java
import java.util.Iterator;
import java.util.stream.Stream;

void main() {
    Stream.of(Iterator.class.getMethods())
            .forEach(System.out::println);
}

我们看到四个方法,其中两个是 default 接口方法

heinz$ java --enable-preview --source 21 MethodsOnIterator.java
public default void java.util.Iterator.remove()
public default void java.util.Iterator.forEachRemaining(java.util.function.Consumer)
public abstract boolean java.util.Iterator.hasNext()
public abstract java.lang.Object java.util.Iterator.next()

如果我们创建一个 java.util.Iterator 类型的对象,我们甚至可以调用这些方法。在下一个示例中,我们查找名为 "forEachRemaining" 并且接受 Consumer 作为参数的方法。然后我们从 List.of() 创建一个 Iterator,并使用反射调用 forEachRemaining 方法。请注意,可能出现一些错误,最显着的是该方法不存在 (NoSuchMethodException) 以及我们无权调用该方法 (IllegalAccessException)。从 Java 7 开始,我们有一个覆盖反射中所有可能出错情况的通用异常,即 ReflectiveOperationException

// MethodsOnIteratorCalling.java
import java.util.List;
import java.util.Iterator;
import java.util.function.Consumer;

void main() throws ReflectiveOperationException {
    var iterator = List.of("Hello", "Dev", "Java").iterator();
    var forEachRemainingMethod = Iterator.class.getMethod(
        "forEachRemaining", Consumer.class);
    Consumer<?> println = System.out::println;
    forEachRemainingMethod.invoke(iterator, println);
}

我们接下来的示例更有趣,如果我可以这么说的话。我们将获取一个 List 项目,然后搜索 Collections 类,看看我们是否可以找到可以提供给该方法的任何方法。我们调用该方法,看看我们的列表发生了什么。由于这些方法在 Collections 中声明为 static,因此我们 invoke() 方法的第一个参数将是 null。我们可以使用流,但它们与检查异常“不兼容”,因此必须使用普通的旧 for-in 循环

// CollectionsListMethods.java
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

void main() throws ReflectiveOperationException {
    var pi = "3141592653589793".chars()
            .map(i -> i - '0')
            .boxed().collect(Collectors.toList());
    System.out.println(pi);
    for (var method : Collections.class.getMethods()) {
        if (method.getReturnType() == void.class
                && method.getParameterCount() == 1
                && method.getParameterTypes()[0] == List.class) {
            System.out.println("Calling " + method.getName() + "()");
            method.invoke(null, pi);
            System.out.println(pi);
        }
    }
}

这很好用,我们找到了三个符合我们要求的方法:sort()shuffle()reverse()。这些方法的顺序没有保证。例如,在 OpenJDK 21 中的 Collections.java 文件中,它们的顺序是 sort()reverse()shuffle()。但是,当我运行代码时,它们显示为

heinz$ java --enable-preview --source 21 CollectionsListMethods.java 
[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3]
Calling reverse()
[3, 9, 7, 9, 8, 5, 3, 5, 6, 2, 9, 5, 1, 4, 1, 3]
Calling sort()
[1, 1, 2, 3, 3, 3, 4, 5, 5, 5, 6, 7, 8, 9, 9, 9]
Calling shuffle()
[5, 7, 4, 9, 9, 9, 2, 1, 6, 5, 3, 3, 1, 5, 3, 8]

 

深度反射访问

到目前为止,我们还没有做任何特别危险的事情。我们发现和调用的所有方法都是 public。唯一有点危险的部分是我们没有编译器检查这些方法是否存在并且可以访问。但是,我们也可以更深入地凝视镜子。

例如,让我们考虑我们的 Person

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return name + " (" + age + ")";
    }
}

由于我们现在使用的是两个独立的类,因此我们需要编译它们。我们可以像以前一样对演示使用无名类,但我们仍然应该编译它们。使用单文件调用将是一个错误,因为在这种情况下,Person 和演示将存在于不同的类加载器中。这会导致难以理解的运行时错误。我曾经花了一整天的时间来追踪这个确切的错误。不要像我一样。

这是我们的 FountainOfYouth.java 文件

// FountainOfYouth.java
import java.lang.reflect.*;

void main() throws ReflectiveOperationException {
    var person = new Person("Heinz Kabutz", 51);
    System.out.println(person);
    Field ageField = Person.class.getDeclaredField("age");
    ageField.setAccessible(true); // deep reflection engaged!
    int age = (int) ageField.get(person);
    age *= .9;
    ageField.set(person, age);
    System.out.println(person);
}

我们首先编译 FountainOfYouth 类,它会传递编译 Person.java。然后我们运行它,瞧,我的年龄减少了 10%。

heinz$ javac --enable-preview --source 21 FountainOfYouth.java 
heinz$ java --enable-preview FountainOfYouth
Heinz Kabutz (51)
Heinz Kabutz (45)

请注意,age 字段是 private 并且 final,但我们仍然能够更改它。如果我们将 Person 转换为记录,那么它将不再允许我们通过深度反射更改属性。

Java 模块系统

深度反射仅在包含该类的模块对我们开放时才有效。理想情况下,我们应该要求模块的作者将包打开到我们的模块。他们很可能会拒绝,而且有充分的理由。通过打开他们的包,他们允许无限制地访问他们最私密的实现细节。如果在不久的将来或遥远的将来,他们想要更改字段名称或类型怎么办?深度反射代码很可能停止工作,他们将不得不永远修复其他模块。

我们可以使用命令行参数--add-opens在另一个模块中打开一个包以进行深度反射,但我们应该只在万不得已的情况下使用它。它非常不可取,我在这里只是厌恶地提一下,但不会展示更多关于如何使用它的细节。

 

结论

我们希望您在这个教程中对反射的工作原理有所了解。还有很多其他主题可以探索:数组、动态代理、泛型、密封类等。我们如何读取记录的属性,如何保留参数名称。但这已经足够长了,希望可以帮助您入门。

要深入了解 Java 编程语言,请务必订阅The Java Specialists' Newsletter,这是一个面向希望提高 Java 技能的任何人的新闻通讯。

上次更新: 2021 年 9 月 14 日


返回教程列表