Java反射机制

password
icon
AI summary
type
status
date
slug
summary
tags
category
Property
Nov 22, 2024 06:19 AM
反射使我们摆脱了只能在编译时执行面向类型操作的限制,并且让我们能够编写一些非常强大的程序。本文将讨论 Java 是如何在运行时发现对象和类的信息的,这通常有两种形式:简单反射,它假定你在编译时就已经知道了所有可用的类型;以及更复杂的反射,它允许我们在运行时发现和使用类的信息。

为什么需要反射


面向对象编程的一个基本目标就是,让编写的代码只操纵基类的引用。我们来看下面这个例子:
Shape 接口中的方法 draw() 是可以动态绑定的,因此客户程序员可以通过泛化的 Shape 引用来调用具体的 draw() 方法。在所有子类中,draw() 都被重写,并且因为它是一个动态绑定的方法,即使通过泛化的 Shape 引用来调用它,也会产生正确的行为,这就是多态。
 
基类里包含一个 draw() 方法,它通过将 this 传递给 System.out.println(),间接地使用了 toString() 方法来显示类的标识符(toString() 方法被声明为 abstract 的,这样就可以强制子类重写该方法,并防止没什么内容的 Shape 类被实例化)。
 
在此示例中,将一个 Shape 的子类对象放入 Stream<Shape> 时,会发生隐式的向上转型,在向上转型为 Shape 时,这个对象的确切类型信息就丢失了,对于流来说,它们只是 Shape 类的对象。
 
从技术上讲,Stream<Shape> 实际上将所有内容都当作 Object 保存。当一个元素被取出时,它会自动转回 Shape,这是反射最基本的形式,在运行时检查了所有的类型转换是否正确,这就是反射的意思:在运行时,确定对象的类型。
 
在这里,反射类型转换并不彻底:Object 只是被转换成了 Shape,而没有转换为最终的 CircleSquareTriangle。这是因为我们所能得到的信息就是,Stream<Shape> 里保存的都是 Shape,在编译时,这是由 Stream 和 Java 泛型系统强制保证的,而在运行时,类型转换操作会确保这一点。
 
接下来就该多态上场了,Shape 对象实际上执行的代码,取决于引用是属于CircleSquare 还是 Triangle。一般来说,这是合理的:你希望自己的代码尽可能少地知道对象的确切类型信息,而只和这类对象的通用表示(在本例中为Shape)打交道。这样的话我们的代码就更易于编写、阅读和维护,并且设计也更易于实现、理解和更改。所以多态是面向对象编程的一个基本目标。

Class 对象


要想了解 Java 中的反射是如何工作的,就必须先了解类型信息在运行时是如何表示的。这项工作是通过叫作 Class 对象的特殊对象来完成的,它包含了与类相关的信息。事实上,Class 对象被用来创建类的所有 “常规” 对象,Java 使用 Class 对象执行反射,即使是类型转换这样的操作也一样。Class 类还有许多其他使用反射的方式。
程序中的每个类都有一个 Class 对象,也就是说,每次编写并编译一个新类时,都会生成一个 Class 对象(并被相应地存储在同名的 .class 文件中)。为了生成这个对象,Java 虚拟机(JVM)使用被称为类加载器(class loader)的子系统。
类加载器子系统实际上可以包含一条类加载器链,但里面只会有一个原始类加载器,它是 JVM 实现的一部分。原始类加载器通常从本地磁盘加载所谓的可信类,包括 Java API 类。
类在首次使用时才会被动态加载到 JVM 中。当程序第一次引用该类的静态成员时,就会触发这个类的加载(构造器是类的一个静态方法,尽管没有明确使用 static 关键字)。因此,使用 new 操作符创建类的新对象也算作对该类静态成员的引用,构造器的初次使用会导致该类的加载。
所以,Java 程序在运行前并不会被完全加载,而是在必要时加载对应的部分,这与许多传统语言不同,这种动态加载能力使得 Java 可以支持很多行为。
类加载器首先检查是否加载了该类型的 Class 对象,如果没有,默认的类加载器会定位到具有该名称的 .class 文件(例如,某个附加类加载器可能会在数据库中查找对应的字节码)。当该类的字节数据被加载时,它们会被验证,以确保没有被损坏,并且不包含恶意的 Java 代码(这是 Java 的众多安全防线里的一条)。
一旦该类型的 Class 对象加载到内存中,它就会用于创建该类型的所有对象:
我们创建了三个具有静态代码块的类,该静态代码块会在第一次加载类时执行,输出的信息会告诉我们这个类是什么时候加载的。输出结果显示了 Class 对象仅在需要时才加载,并且静态代码块的初始化是在类加载时执行的。
所有的 Class 对象都属于 Class 类,Class 对象和其他对象一样,因此你可以获取并操作它的引用(这也是加载器所做的)。静态的 forName() 方法可以获得 Class 对象的引用,该方法接收了一个包含所需类的文本名称(注意拼写和大小写,且需要是类的完全限定名称,即包括包名称)的字符串,并返回了一个 Class 引用。
不管什么时候,只要在运行时用到类型信息,就必须首先获得相应的 Class 对象的引用,这时 Class.forName() 方法用起来就很方便了,因为不需要对应类型的对象就能获取 Class 引用。但是,如果已经有了一个你想要的类型的对象,就可以通过 getClass() 方法来获取 Class 引用,这个方法属于 Object 根类,它返回的 Class 引用表示了这个对象的实际类型。
Class 类有很多方法,下面是其中的一部分:
printInfo() 方法使用 getName() 来生成完全限定的类名,使用 getSimpleName()getCanonicalName() 分别生成不带包的名称和完全限定的名称,isInterface() 可以告诉你这个 Class 对象是否表示一个接口,getInterfaces() 方法返回了一个 Class 对象数组,它们表示所调用的 Class 对象的所有接口。还可以使用 getSuperclass() 来查询 Class 对象的直接基类,它将返回一个 Class 引用,而你可以对它做进一步查询。
ClassnewInstance() 方法是实现虚拟构造器的一种途径,这相当于声明:我不知道你的确切类型,但无论如何你都要正确地创建自己。sc 只是一个 Class 引用,它在编译时没有更多的类型信息,当创建一个新实例时,你会得到一个 Object 引用,但该引用指向了一个 Toy 对象,你可以给它发送 Object 能接收的消息,但如果想要发送除此之外的其他消息,就必须进一步了解它,并进行某种类型转换。此外,使用 Class.newInstance() 创建的类必须有一个无参构造器。
注意,此示例中的 newInstance() 在 Java 8 中还是正常的,但在更高版本中已被弃用,Java 推荐使用 Constructor.newInstance() 来代替。

2.1 类字面量

Java 还提供了另一种方式来生成 Class 对象的引用:类字面量。它看起来像这样:
这更简单也更安全,因为它会进行编译时检查(因此不必放在 try 块中),另外它还消除了对 forName() 方法的调用,所以效率也更高。
注意,使用 .class 的形式创建 Class 对象的引用时,该 Class 对象不会自动初始化。实际上,在使用一个类之前,需要先执行以下三个步骤:
  • 加载:这是由类加载器执行的,该步骤会先找到字节码(通常在类路径中的磁盘上,但也不一定),然后从这些字节码中创建一个 Class 对象。
  • 链接:链接阶段会验证类中的字节码,为静态字段分配存储空间,并在必要时解析该类对其他类的所有引用。
  • 初始化:如果有基类的话,会先初始化基类,执行静态初始化器和静态初始化块。
其中,初始化会被延迟到首次引用静态方法(构造器是隐式静态的)或非常量静态字段时:
仅使用 .class 语法来获取对类的引用不会导致初始化,而 Class.forName() 会立即初始化类以产生 Class 引用。如果一个 static final 字段的值是编译时常量,比如 A.STATIC_FINAL,那么这个值不需要初始化 A 类就能读取。

2.2 泛型类的引用

Class 引用指向的是一个 Class 对象,该对象可以生成类的实例,并包含了这些实例所有方法的代码,它还包含该类的静态字段和静态方法,所以一个 Class 引用表示的就是它所指向的确切类型:Class 类的一个对象。
我们可以使用泛型语法来限制 Class 引用的类型:
泛化的类引用 c2 只能分配给其声明的类型,通过使用泛型语法,可以让编译器强制执行额外的类型检查。
如果想放松使用泛化的 Class 引用时的限制,需要使用通配符 ?,它是 Java 泛型的一部分,表示任何事物:
我们不能这么写:
即使 Integer 继承自 Number,但是 IntegerClass 对象不是 NumberClass 对象的子类。
如果想创建一个 Class引用,并将其限制为某个类型或任意子类型,可以将通配符与 extends 关键字组合来创建一个界限
将泛型语法添加到 Class 引用的一个原因是提供编译时的类型检查,这样的话,如果你做错了什么,那么很快就能发现。
下面是一个使用了泛型类语法的示例,它存储了一个类引用,然后使用 newInstance() 来生成对象:
DynamicSupplier 会强制要求它使用的任何类型都有一个 public 的无参构造器,如果不符合条件,就会抛出一个异常。在上面的例子中,People 类自动生成的无参构造器不是 public 的,因为 People 类不是 public 的,所以我们必须显式定义它。
Class 对象使用泛型语法时,newInstance() 会返回对象的确切类型,而不仅仅是简单的 Object,但它也会受到一些限制:
如果你得到了 Kitty 的基类,那么编译器只允许你声明这个基类引用是 Kitty某个基类,即 Class<? super Kitty>,而不能被声明成 Class<Cat>,因为 getSuperclass() 返回了基类(不是接口),而编译器在编译时就知道这个基类是什么,在这里就是 Cat.class,而不仅仅是 Kitty 的某个基类。因为存在这种模糊性,所以 kittySuper.getConstructor().newInstance() 的返回值不是一个确切的类型,而只是一个 Object

2.3 cast() 方法

cast() 方法是用于 Class 引用的类型转换:
cast() 方法接收参数对象并将其转换为 Class 引用的类型,在你不能使用普通类型转换(最后一行)的情况下很有用,如果你正在编写泛型代码并且存储了一个用于转型的 Class 引用,就可能会遇到这种情况,不过这很罕见。
上一篇
前端三件套:vue,tailwindcss和element-plus
下一篇
Java知识库(to 飞书)
Loading...