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
,而没有转换为最终的 Circle
、Square
或 Triangle
。这是因为我们所能得到的信息就是,Stream<Shape>
里保存的都是 Shape
,在编译时,这是由 Stream
和 Java 泛型系统强制保证的,而在运行时,类型转换操作会确保这一点。接下来就该多态上场了,
Shape
对象实际上执行的代码,取决于引用是属于Circle
、Square
还是 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
引用,而你可以对它做进一步查询。Class
的 newInstance()
方法是实现虚拟构造器的一种途径,这相当于声明:我不知道你的确切类型,但无论如何你都要正确地创建自己。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
,但是 Integer
的 Class
对象不是 Number
的 Class
对象的子类。如果想创建一个
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...