一、类的加载、连接和初始化
系统可能在第一次使用某个类时加载该类,也可能采用预加载机制 来加载某个类。
(一)、JVM 和 类
当调用 java 命令运行某个 Java 程序时,该命令将会启动一个 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。正如前面介绍的,同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区
。 当系统出现以下几种情况时,JVM进程将被终止。
➢ 程序运行到最后正常结束。
➢ 程序运行到使用 System.exit() 或 Runtime.getRuntime().exit() 代码处结束程序。
➢ 程序执行过程中遇到未捕获的异常或错误而结束。
➢ 程序所在平台强制结束了JVM进程。
同一个类的所有实例的静态变量共享同一块内存区, 如果在同一个程序中,同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该 JVM 进程的内存区。
public class A {
public static int a = 5;
}
public class Test1 {
public static void main(String[] args) {
System.out.println(A.a); // 5
A a = new A();
a.a++;
System.out.println(a.a); // 6
A b = new A();
System.out.println(b.a); // 6
}
}
如果是用不同程序,即使访问同一个静态成员变量,也不会彼此影响,因为这两个程序处于不同的 JVM 进程中,两个 JVM 之间并不会共享数据
Test1
public class Test1 {
public static void main(String[] args) {
A a = new A();
a.a++;
System.out.println(a.a); // 6
}
}
Test2
public class Test2 {
public static void main(String[] args) {
A b = new A();
System.out.println(b.a); // 5
}
}
(二)、类的加载
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统 会通过加载、连接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM 将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化。
类加载指的是将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象,也就是说,当程序中使用任何类时,系统都会为之建立一个 java.lang.Class 对象。
类是某一类对象的抽象,类是概念层次的东西。但:类也是一种对象。就像平常说概念主要用于定义、描述其他事物,但概念本身也是一种事物,那么概念本身也需要被描述——这有点像一个哲学命题。但事实就是这样,每个类是一批具有相同特征的对象的抽象(或者说概念),而系统中所有的类实际上也是实例,它们都是java.lang.Class的实例。
类的加载由类加载器完成,类加载器通常由 JVM 提供,这些类加载器也是前面所有程序运行的基础,JVM 提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承 ClassLoader 基类来创建自己的类加载器
。通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
➢ 从本地文件系统加载 class 文件,这是前面绝大部分示例程序的类加载方式。
➢ 从JAR 包加载class文件,这种方式也是很常见的,JDBC编程时用到的数据库驱动类就放在 JAR 文件中,JVM 可以从 JAR 文件中直接加载该class文件。
➢ 通过网络加载 class 文件。
➢ 把一个Java源文件动态编译,并执行加载。
类加载器通常无须等到“首次使用”该类时才加载该类,Java 虚拟机规范允许系统预先加载某些类。
(三)、类的连接
当类被加载之后,系统为之生成一个对应的 Class 对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接 又可分为如下三个阶段。
(1)验证:验证阶段用于检验被加载的类是否有正确的内部结 构,并和其他类协调一致。
(2)准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值。
(3)解析:将类的二进制数据中的符号引用替换成直接引用。
(四)、类的初始化
在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变 量进行初始化。在Java类中对类变量指定初始值有两种方式:
① 声明类变量时指定初始值;
② 使用静态初始化块为类变量指定初始值。
public class Test {
// 申明变量 a 时指定初始值 a=5;
static int a = 5;
// 没有指定初始值,默认 b=0;
static int b;
// 使用静态代码块为变量 c 指定初始值 c=10;
static int c;
static {
c = 10;
}
}
声明变量时指定初始值,静态初始化块都将被当成类的初始化语句,JVM 会按这些语句在程序中的排列顺序依次执行它们
public class Test {
static {
// 使用静态代码块指定 c 初始值
c = 10;
System.out.println("==========");
}
// 申明变量 a 时指定初始值 a=5;
static int a = 5;
// 没有指定初始值,默认 b=0;
static int b;
// 使用静态代码块为变量 c 指定初始值 c=10;
static int c = 2; // ①
public static void main(String[] args) {
System.out.println(Test.c); // 2
}
}
上面代码先在静态初始化块中为 c 变量赋值,此时类变量 c 的值为 10;接着程序向下执行,执行到 ① 号代码处,这行代码也属于该类的初始化语句,所以程序再次为类变量 c 赋值。也就是说,当Test类初始化结束后,该类的类变量 c 的值为 2。
JVM初始化一个类包含如下几个步骤。
① 假如这个类还没有被加载和连接,则程序先加载并连接该类。
② 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
③ 假如类中有初始化语句,则系统依次执行这些初始化语句。
当执行第 2 个步骤时,系统对直接父类的初始化步骤也遵循此步骤1~3;如果该直接父类又有直接父类,则系统再次重复这三个步骤来先 初始化这个父类……依此类推,所以 JVM 最先初始化的总是 java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。
(五)、类初始化的时机
当Java程序首次通过下面 6 种方式来使用某个类或接口时,系统就会初始化该类或接口。
➢ 创建类的实例。为某个类创建实例的方式包括:使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
➢ 调用某个类的类方法(静态方法)。
➢ 访问某个类或接口的类变量,或为该类变量赋值。
➢ 使用反射方式来强制创建某个类或接口对应的java.lang.Class 对象。例如代码:Class.forName(“Person”),如果系统还未 初始化Person类,则这行代码将会导致该 Person 类被初始化,并返回 Person 类对应的 java.lang.Class 对象。
➢ 初始化某个类的子类。当初始化某个类的子类时,该子类的所 有父类都会被初始化。
➢ 直接使用 java.exe 命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。
除此之外,下面的几种情形需要特别指出。
对于一个 final 型的类变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于“宏变量”。Java 编译器会在编译时直接把这个类变量出现的地方替换成它的值因此即使程序使用该静态类变量,也不会导致该类的初始化。
class Test1 {
static {
System.out.println("静态初始化块");
}
static final String msg = "你好,zuijin";
}
public class MsgTest {
public static void main(String[] args) {
System.out.println(Test1.msg); // ①
}
}
控制台输出
上面程序的 Test1 类中有一个 msg 的类变量,该类变量使用了 final 修饰,而且它的值可以在编译时确定下来,因此 msg 会被当成“宏变量”处理。程序中所有使用 msg 的地方都会在编译时被直接替换成它的值——也就是说,上面程序中 ① 号粗体字代码在编译时就会被替换成 “你好,zuijin”,所以该代码不会导致初始化Test1 类。
提示
当某个类变量(也叫静态变量)使用了final修饰,而且它的值 可以在编译时就确定下来,那么程序其他地方使用该类变量时,实际上并没有使用该类变量,而是相当于使用常量。
反之,如果 final 修饰的类变量的值不能在编译时确定下来,则必须等到运行时才可以确定该类变量的值,如果通过该类来访问它的类变量,则会导致该类被初始化。
class Test1 {
static {
System.out.println("静态初始化块");
}
// msg 的值不能在编译时确定下来
static final String msg = String.valueOf(System.currentTimeMillis());
}
public class MsgTest {
public static void main(String[] args) {
System.out.println(Test1.msg);
}
}
输出
因为上面定义的 msg 类变量的值必须在运行时才可以确定,所以 ① 处的粗体字代码必须保留为对 Test1 类的类变量的引用,这行代码就变成了使用 Test1 的类变量,这将导致 Test1 类被初始化。
当使用 ClassLoader 类的 loadClass() 方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用 Class 的 forName() 静态方法才会导致强制初始化该类。例如如下代码。
class Tester {
static {
System.out.println("Tester 类的静态初始化块。。。");
}
}
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// 下面语句仅仅是加载 Tester 类
classLoader.loadClass("main.classs.Tester");
System.out.println("系统加载 Tester 类");
// 下面的语句才会初始化 Tester 类
Class.forName("main.classs.Tester");
}
}
输出
从上面的运行结果可以看出,必须等到执行 Class.forName(“Tester”) 时才完成对 Tester 类的初始化。
二、类加载器
类加载器负责将 .class 文件(可能在磁盘上,也可能在网络上)加 载到内存中,并为之生成对应的java.lang.Class 对象。尽管在Java开发中无须过分关心类加载机制,但所有的编程人员都应该了解其工作机制,明白如何做才能让其更好地满足我们的需要。
(一)、类加载机制
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个 java.lang.Class 实例。一旦一个类被载入JVM中,同一个类就不会被 再次载入了。现在的问题是,怎么样才算“同一个类”?
正如一个对象有一个唯一的标识一样,一个载入JVM中的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名) 作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为唯一标识。例如,如果在 pg 的包中有一个名为 Person 的类,被类加载器 ClassLoader 的实例kl负责加载,则该Person 类对应的 Class 对象在 JVM 中表示为(Person、pg、kl)。这意味着两个类加载器加载的同名类:(Person、pg、kl)和(Person、pg、kl2)是不同的,它们所加载的类也是完全不同、互不兼容的。
当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。
➢ Bootstrap ClassLoader:根类加载器。
➢ Extension ClassLoader:扩展类加载器。
➢ System ClassLoader:系统类加载器。
Bootstrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载 Java 的核心类。在 Sun的 JVM 中,当执行 java.exe 命令时,使用 -Xbootclasspath 或 -D 选项指定sun.boot.class.path 系统属性 值可以指定加载附加的类。
JVM的类加载机制主要有如下三种。
➢ 全盘负责。
所谓全盘负责,就是当一个类加载器负责加载某个 Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
➢ 父类委托。
所谓父类委托,则是先让parent(父)类加载器试 图加载该Class,只有在父类加载器无法加载该类时才尝试从自 己的类路径中加载该类。
➢ 缓存机制。
缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区中。
这就是为什么修改了 Class 后,必须重新启动JVM,程序所做的修改才会生效的原因。
提示
类加载器之间的父子关系并不是类继承上的父子关系,这里的父 子关系是类加载器实例之间的关系。
除了可以使用 Java 提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承ClassLoader 来实现。JVM 中这 4 种类加载器的层次结构如图所示。

访问 JVM 的类加载器
public class ClassLoaderPropTest {
public static void main(String[] args) throws IOException {
// 获取系统类加载器
ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + sysLoader);
/*
* 获取系统类加载器的加载路径——通常由 CLASSPATH 环境变量指定
* 如果操作系统没有指定 CLASSPATH 环境变量,则默认以当前路径
* 作为系统类加载器的加载路径
* */
Enumeration<URL> eml = sysLoader.getResources("");
while (eml.hasMoreElements()) {
System.out.println(eml.nextElement());
}
// 获取系统类加载器的父类加载器,得到扩展类加载器
ClassLoader extensionLoader = sysLoader.getParent();
System.out.println("扩展类加载器:" + extensionLoader);
System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的 parent:" + extensionLoader.getParent());
}
}
运行结果
系统类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
file:/D:/IDEA_Project/java-base/out/production/java-base/
扩展类加载器:jdk.internal.loader.ClassLoaders$PlatformClassLoader@17550481
扩展类加载器的加载路径:null
扩展类加载器的 parent:null
从上面运行结果可以看出,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是 null(与Java 8有区别),但此处看到扩展类加载器的父加载器是 null,并不是根类加载器。这是因为根类加载器并没有继承 ClassLoader 抽象类,所以扩展类加载器的 getParent() 方法返回null。但实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是 Java 实现的。
从运行结果可以看出,系统类加载器是 AppClassLoader 的实例,扩展类加载器 PlatformClassLoader的实例。实际上,这两个类都是 URLClassLoader 类的实例。
注意:
JVM 的根类加载器并不是Java实现的,而且由于程序通常无须访问根类加载器,因此访问扩展类加载器的父类加载器时返回 null。
类加载器加载Class大致要经过如下8个步骤。
① 检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则接着执行第2步。
② 如果父类加载器不存在(如果没有父类加载器,则要么 parent 一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行; 如果父类加载器存在,则接着执行第3步。
③ 请求使用父类加载器去载入目标类,如果成功载入则跳到第8 步,否则接着执行第5步。
④ 请求使用根类加载器来载入目标类,如果成功载入则跳到第8 步,否则跳到第7步。
⑤ 当前类加载器尝试寻找Class文件(从与此 ClassLoader 相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。
⑥ 从文件中载入 Class,成功载入后跳到第8步。
⑦ 抛出 ClassNotFoundException 异常。
⑧ 返回对应的 java.lang.Class 对象。
其中,第5、6步允许重写 ClassLoader 的 findClass() 方法来实现自己的载入策略,甚至重写loadClass() 方法来实现自己的载入过程。
(二)、创建并使用自定义的类加载器 // TODO,程序运行问题
JVM 中除根类加载器之外的所有类加载器都是 ClassLoader 子类的实例,开发者可以通过扩展ClassLoader 的子类,并重写该 ClassLoader 所包含的方法来实现自定义的类加载器。查阅API文档中关于ClassLoader 的方法不难发现,ClassLoader 中包含了大量的 protected 方法——这些方法都可被子类重写。
ClassLoader 类有如下两个关键方法。
➢ loadClass(String name, boolean resolve) : 该方法为 ClassLoader 的入口点,根据指定名称来加载类,系统就是调用 ClassLoader 的该方法来获取指定类对应的 Class 对象。
➢ findClass(String name):根据指定名称来查找类。
如果需要实现自定义的 ClassLoader,则可以通过重写以上两个方法来实现,通常推荐重写 findClass() 方法,而不是重写 loadClass() 方法。
loadClass() 方法的执行步骤如下。
① 用 findLoadedClass(String) 来检查是否已经加载类,如果已经加载则直接返回。
② 在父类加载器上调用 loadClass() 方法。如果父类加载器为 null,则使用根类加载器来加载。
③ 调用 findClass(String) 方法查找类。
从上面步骤中可以看出,重写 findClass() 方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写 loadClass() 方法,则实现逻辑更为复杂。
在 ClassLoader 里还有一个核心方法:
Class defineClass(String name, byte[] b, int off, int len)
该方法负责将指定类的字节码 文件(即Class文件,如Hello.class)读入字节数组byte[] b内,并把它转换为 Class 对象,该字节码文件可以来源于文件、网络等。 defineClass() 方法管理 JVM 的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。不过不用担心,程序员无须重写该方法。实际上该方法是final的,即使想重写也没有机会。
除此之外,ClassLoader 里还包含如下一些普通方法。
➢ findSystemClass(String name):从本地文件系统装入文件。 它在本地文件系统中寻找类文件,如果存在,就使用 defineClass() 方法将原始字节转换成 Class 对象,以将该文件转换成类。
➢ static getSystemClassLoader():这是一个静态方法,用于返 回系统类加载器。
➢ getParent():获取该类加载器的父类加载器。
➢ resolveClass(Class<?> c):链接指定的类。类加载器可以使用此方法来链接类 c。无须理会关于此方法的太多细节。
➢ findLoadedClass(String name):如果此 Java 虚拟机已加载了名为 name 的类,则直接返回该类对应的 Class 实例,否则返回 null。该方法是 Java 类加载缓存机制的体现。
下面程序开发了一个自定义的 ClassLoader,该 ClassLoader 通过重写 findClass() 方法来实现自定义的类加载机制。这个 ClassLoader 可以 在加载类之前先编译该类的源文件,从而实现运行Java之前先编译该程序的目标,这样即可通过该 ClassLoader 直接运行 Java 源文件。
1、CompileClassLoader.java 自定义类加载器
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* <p>
*
* </p>
*
* @author 醉瑾
* @since 2022-07-06
*/
public class CompileClassLoader extends ClassLoader {
// 读取一个文件的内容
private byte[] getBytes(String filename) throws IOException {
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int) len];
try (var fin = new FileInputStream(file)) {
// 一次读取 Class 文件的全部二进制数据
int r = fin.read(raw);
if (r != len) {
throw new IOException("无法读取全部文件" + r + "!=" + len);
}
return raw;
}
}
// 定义编译指定 Java 文件的方法
private boolean compile(String javaFile) throws IOException {
System.out.println("CompileClassLoader:正在编译" + javaFile + "....");
// 调用系统的 javac 命令,javac 后面有空格,不能忘
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try {
// 其他线程都等待这个线程完成
p.waitFor();
} catch (InterruptedException ie) {
System.out.println(ie.getMessage());
}
// 获取 javac 线程的退出值
int ret = p.exitValue();
// 返回是否编译成功
return ret == 0;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
// 将包路径中的点(.)替换为斜线(/)
String fileStub = name.replace(".", "/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
/*
* 当指定的 Java 源文件存在,且 Class 文件不存在,或者
* Java 源文件的修改时间比 Class 文件的修改时间更晚时
* 重新编译
* */
if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) {
try {
// 如果编译失败或者该 class 文件不存在
if (!compile(javaFilename) || !classFile.exists()) {
throw new ClassNotFoundException("ClassNotFoundException:" + javaFilename);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
// 如果 Class 文件存在,系统负责将该文件转换成 Class 对象
if (classFile.exists()) {
try {
// 将 Class 文件的二进制数据读入数组
byte[] raw = getBytes(classFilename);
// 调用 ClassLoader 的 defineClass 方法将二进制数据转换为 Class 对象
clazz = defineClass(name, raw, 0, raw.length);
} catch (IOException ie) {
ie.printStackTrace();
}
}
// 如果 clazz 为 null,表明加载失败,则抛出异常
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
System.out.println("程序开始执行------------------------");
// 如果运行该程序时没有参数,即没有目标类
if (args.length < 1) {
System.out.println("缺少目标类,请按如下格式运行 Java 源文件");
System.out.println("java CompileClassLoader ClassName");
}
// 第一个参数是需要运行的类
String progClass = args[0];
// 剩下的参数将作为运行目标类时的参数
// 将这些参数复制到一个新数组中
String[] progArgs = new String[args.length - 1];
System.arraycopy(args, 1, progArgs, 0, progArgs.length);
CompileClassLoader ccl = new CompileClassLoader();
// 加载需要运行的类
Class<?> clazz = ccl.loadClass(progClass);
// 获取需要运行的类的主方法
Method main = clazz.getMethod("main", (String[].class));
Object[] argsArray = {progArgs};
main.invoke(null, argsArray);
}
}
2、Hello.java 测试文件
public class Hello {
public static void main(String[] args) {
for (String arg : args) {
System.out.println("运行Hello 的参数" + arg);
}
}
}
进入文件所在目录打开命令行终端
提示
命令行运行时 java 文件中不要有 package xxx.xxx
(1)首先执行以下语句编译 CompileClassLoader.java 文件
javac -encoding UTF-8 CompileClassLoader.java
(2)使用如下命令来运行该 Hello.java 程序
java CompileClassLoader Hello 醉瑾
本示例程序提供的类加载器功能比较简单,仅仅提供了在运行之前 先编译Java源文件的功能。实际上,使用自定义的类加载器,可以实现如下常见功能。
➢ 执行代码前自动验证数字签名。
➢ 根据用户提供的密码解密代码,从而可以实现代码混淆器来避 免反编译*.class文件。
➢ 根据用户需求来动态地加载类。
➢ 根据应用需求把其他数据以字节码的形式加载到应用中。
(三)、URLClassLoader 类
Java 为 ClassLoader 提供了一个 URLClassLoader 实现类,该类也是系统类加载器和扩展类加载器的父类(此处的父类,就是指类与类之间 的继承关系)。URLClassLoader 功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。在应用程序中可以直接使用 URLClassLoader 加载类,URLClassLoader 类提供了如下两个构造器。
➢ URLClassLoader(URL[] urls):使用默认的父类加载器创建一个 ClassLoader 对象,该对象将从 urls 所指定的系列路径来查询并加载类。
➢ URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父类加载器创建一个 ClassLoader 对象,其他功能与前一个构造器相同。
一旦得到了 URLClassLoader 对象之后,就可以调用该对象的 loadClass() 方法来加载指定类。下面程序示范了如何直接从文件系统中加载 MySQL 驱动,并使用该驱动来获取数据库连接。通过这种方式来获取数据库连接,可以无须将 MySQL 驱动添加到 CLASSPATH 环境变量中。
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.sql.*;
import java.util.*;
import java.net.*;
/**
* <p>
*
* </p>
*
* @author 醉瑾
* @since 2022-07-08
*/
public class URLClassLoaderTest {
private static Connection conn;
// 定义一个数据库连接方法
public static Connection getConn(String url, String user, String pwd) throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, SQLException {
if (conn == null) {
// 创建一个 URL 数组
URL[] urls = {new URL("file:mysql-connector-java-8.0.13.jar")};
// 以默认的 ClassLoader 作为父 ClassLoader,创建 URLClassLoader
Driver driver;
try (URLClassLoader myClassLoader = new URLClassLoader(urls)) {
System.out.println(myClassLoader);
// 加载 MySQL 的 JDBC 驱动,并创建默认实例
driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance();
} catch (IOException | InvocationTargetException e) {
throw new RuntimeException(e);
}
// 创建一个设置 JDBC 连接属性的 Properties 对象
Properties props = new Properties();
// 至少需要为该对象传入 user 和 password 两个属性
props.setProperty("user", user);
props.setProperty("password", pwd);
// 调用 Driver 对象的 connect 方法来取得数据库连接
conn = driver.connect(url, props);
}
return conn;
}
public static void main(String[] args) throws MalformedURLException, SQLException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
String url = "jdbc:mysql://localhost:3306/mysql?serverTimezone=UTC";
String user = "root";
String pwd = "admin";
Connection conn1 = getConn(url, user, pwd);
System.out.println(conn1);
}
}
上面程序中代码创建了一个 URLClassLoader 对象, 该对象使用默认的父类加载器,该类加载器的类加载路径是当前路径下的 mysql-connector-java-8.0.13.jar文件,将 MySQL 驱动复制到该路径下,这样保证该 ClassLoader 可以正常加载到 com.mysql.jdbc.Driver 类。
正如前面所看到的,创建URLClassLoader时传入了一个URL数组参数,该 ClassLoader 就可以从这系列 URL 指定的资源中加载指定类,这里的 URL 可以以file: 为前缀,表明从本地文件系统加载;可以以 http: 为前缀,表明从互联网通过 HTTP 访问来加载;也可以以 ftp: 为前缀,表明从互联网通过FTP访问来加载……功能非常强大。
三、通过反射查看类信息
Java程序中的许多对象在运行时都会出现两种类型:编译时类型和 运行时类型,例如代码:
Person p=new Student();
这行代码将会生 成一个 p 变量,该变量的编译时类型为 Person,运行时类型为Student;除此之外,还有更极端的情形,程序在运行时接收到外部传入的一个对象,该对象的编译时类型是Object,但程序又需要调用该对象运行时类 型的方法。 为了解决这些问题,程序需要在运行时发现对象和类的真实信息。 解决该问题有以下两种做法。
➢ 第一种做法是假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用 instanceof 运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量即可。
➢ 第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。
(一)、获得 Class 对象
前面已经介绍过了,每个类被加载之后,系统就会为该类生成一个对应的 Class 对象,通过该 Class 对象就可以访问到 JVM 中的这个类。在 Java 程序中获得 Class 对象通常有如下三种方式。
➢ 使用 Class 类的 forName(String clazzName) 静态方法。该方法 需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)。
➢ 调用某个类的 class 属性来获取该类对应的 Class 对象。例如, Person.class 将会返回 Person 类对应的 Class 对象。
➢ 调用某个对象的 getClass() 方法。该方法是 java.lang.Object 类中的一个方法,所以所有的 Java 对象都可以调用该方法,该方法将会返回该对象所属类对应的 Class 对象。
对于第一种方式和第二种方式都是直接根据类来取得该类的 Class 对象,相比之下,第二种方式有如下两种优势。
➢ 代码更安全。程序在编译阶段就可以检查需要访问的 Class 对象是否存在。
➢ 程序性能更好。因为这种方式无须调用方法,所以性能更好。
也就是说,大部分时候都应该使用第二种方式来获取指定类的 Class 对象。但如果程序只能获得一个字符串 ,例如 “java.lang.String”,若需要获取该字符串对应的 Class 对象,则只能使用第一种方式,使用 Class 的 forName(String clazzName) 方法获取 Class 对象时,该方法可能抛出一个 ClassNotFoundException 异常。
一旦获得了某个类所对应的 Class 对象之后,程序就可以调用 Class 对象的方法来获得该对象和该类的真实信息了。
(二)、从 Class 中获取信息
Class 类提供了大量的实例方法来获取该 Class 对象所对应类的详细信息,Class 类大致包含如下方法,下面每个方法都可能包括多个重载的版本,应该查阅 API 文档来掌握它们。
下面4个方法用于获取Class对应类所包含的构造器。
➢ Connstructor<T> getConstructor(Class<?> …parameterTypes)
返回此Class对象对应类的、带指定形参 列表的public构造器。
➢ Constructor<?>[] getConstructors()
返回此Class对象对应类的所有public 构造器。
➢ Constructor<T> getDeclaredConstructor(Class<?> …parameterTypes)
返回此 Class 对象对应类的、带指定形参列表的构造器,与构造器的访问权限无关。
➢ Constructor<?>[] getDeclaredConstructors()
返回此 Class 对象对应类的所有构造器,与构造器的访问权限无关。
下面 4 个方法用于获取 Class 对应类所包含的方法。
➢ Method getMethod(String name, Class<?> …parameterTypes)
返回此Class对象对应类的、带指定形参列表的public方法。
➢ Method[] getMethods()
返回此Class对象所表示的类的所有 public方法。
➢ Method getDeclaredMethod(String name, Class<?> …parameterTypes)
返回此 Class 对象对应类的、带指定形参列表的方法,与方法的访问权限无关。
➢ Method[] getDeclaredMethods()
返回此Class对象对应类的全部方法,与方法的访问权限无关。
上面的多个 getMethod() 方法和 getConstructor() 方法中,都需要传入多个类型为 Class<?> 的参数,用于获取指定的方法或指定的构造器。
关于这个参数的作用,假设某个类内包含如下三个 info 方法签名。
➢ public void info()
➢ public void info(String str)
➢ public void info(String str,Integer num)
这三个同名方法属于重载,它们的方法名相同,但参数列表不同。 在Java语言中要确定一个方法光有方法名是不行的,如果仅仅只指定 info 方法——实际上可以是上面三个方法中的任意一个!如果需要确定一个方法,则应该由方法名和形参列表来确定,但形参名没有任何实际意义,所以只能由形参类型来确定。
例如想指定第二个 info 方法,则必须指定方法名为 info,形参列表为 String.class,如下为获取 info 方法的示例。
InfoTest.java
public class InfoTest {
public void info() {
System.out.println("无参");
}
public void info(String str) {
System.out.println("一个参数,为 " + str);
}
public void info(String str, Integer num) {
System.out.println("两个参数,为 " + str + " 数字 " + num);
}
}
GetInfoTest.java
public class GetInfoTest {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
var infoTest = InfoTest.class.getDeclaredConstructor().newInstance();
// 获取第一个方法
Method info1 = InfoTest.class.getMethod("info");
info1.invoke(infoTest);
// 获取第二个方法
Method info2 = InfoTest.class.getMethod("info", String.class);
info2.invoke(infoTest, "你好");
// 获取第三个方法
Method info3 = InfoTest.class.getMethod("info", String.class, Integer.class);
info3.invoke(infoTest, "你好", 18);
}
}
执行结果如下
提示
获取构造器时无须传入构造器名——同一个类的所有构造器的名字 都是相同的,所以要确定一个构造器只要指定形参列表即可。
如下 4 个方法用于访问 Class 对应类所包含的成员变量。
➢ Field getField(String name)
返回此Class对象对应类的、指定名称的public成员变量。
➢ Field[] getFields()
返回此 Class 对象对应类的所有 public 成员变量。
➢ Field getDeclaredField(String name)
返回此 Class 对象对应类的、指定名称的成员变量,与成员变量的访问权限无关。
➢ Field[] getDeclaredFields()
返回此Class对象对应类的全部成员变量,与成员变量的访问权限无关。
如下几个方法用于访问 Class 对应类上所包含的 Annotation。
➢ <A extends Annotation> A getAnnotation(Class<A>
annotationClass)尝试获取该Class对象对应类上存在的、指定类型的 Annotation;如果该类型的注解不存在,则返回null。
➢ <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass)
这是 Java8 新增的方法,该方法尝试获取直接修饰该 Class 对象对应类的、指定类型的 Annotation;如果该类型的注解不存在,则返回null。
➢ Annotation[] getAnnotations()
返回修饰该Class对象对应 类上存在的所有Annotation。
➢ Annotation[] getDeclaredAnnotations()
返回直接修饰该 Class 对应类的所有 Annotation。
➢ <A extends Annotation> A[]
getAnnotationsByType(Class<A> annotationClass)该方法的功能与前面介绍的 getAnnotation()方法基本相似。但由于 Java8 增加了重复注解功能,因此需要使用该方法获取修饰该类的、指定类型的多个 Annotation。
➢ <A extends Annotation> A[]
getDeclaredAnnotationsByType(Class<A> annotationClass)该方法的功能与前面介绍的 getDeclaredAnnotations() 方法基本相似。但由于 Java8 增加了重复注解功能,因此需要使用该方法获取直接修饰该类的、指定类型的多个Annotation。
如下方法用于访问该 Class 对象对应类包含的内部类。
➢ Class<?>[] getDeclaredClasses()
返回该Class对象对应类里包含的全部内部类。
如下方法用于访问该Class对象对应类所在的外部类。
➢ Class<?> getDeclaringClass()
返回该Class对象对应类所在的外部类。
如下方法用于访问该Class对象对应类所实现的接口。
➢ Class<?>[] getInterfaces()
返回该Class对象对应类所实现的全部接口。
如下方法用于访问该Class对象对应类所继承的父类。
➢ Class<? super T > getSuperclass()
返回该Class对象对应类的超类的Class对象。
如下方法用于获取 Class 对象对应类的修饰符、所在包、类名等基本信息。
➢ int getModifiers()
返回此类或接口的所有修饰符。修饰符由 public、protected、private、final、static、abstract 等对应的常量组成,返回的整数应使用 Modifier 工具类的方法来解码,才可以获取真实的修饰符。
➢ Package getPackage()
获取此类的包。
➢ String getName()
以字符串形式返回此 Class 对象所表示的类的名称。
➢ String getSimpleName()
以字符串形式返回此 Class 对象所表示的类的简称。
除此之外,Class 对象还可调用如下几个判断方法来判断该类是否为接口、枚举、注解类型等。
➢ boolean isAnnotation()
返回此 Class 对象是否表示一个注解 类型(由@interface定义)。
➢ boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
判断此 Class 对象是否使用了Annotation修 饰。
➢ boolean isAnonymousClass()
返回此 Class 对象是否是一个匿 名类。
➢ boolean isArray()
返回此 Class 对象是否表示一个数组类。
➢ boolean isEnum()
返回此 Class 对象是否表示一个枚举(由 enum关键字定义)。
➢ boolean isInterface()
返回此 Class 对象是否表示一个接口 (使用interface定义)。
➢ boolean isInstance(Object obj):判断obj是否是此 Class 对象的实例,该方法可以完全代替 instanceof 操作符。
从 Class 中获取信息详细示例
package main.info;
import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* <p>
*
* </p>
*
* @author 醉瑾
* @since 2022-07-08
*/
@Repeatable(Annos.class)
@interface Anno {
}
@Retention(value = RetentionPolicy.RUNTIME)
@interface Annos {
Anno[] value();
}
@SuppressWarnings(value = "unchecked")
@Deprecated
// 使用重复注解
@Anno
@Anno
public class ClassTest {
// 为该类定义一个私有的构造器
private ClassTest() {
}
// 定义一个有参数的构造器
public ClassTest(String name) {
System.out.println("执行有参数的构造器");
}
// 定义一个无参数的 info 方法
public void info() {
System.out.println("执行无参的 info 方法");
}
// 定义一个有参数的 info 方法
public void info(String str) {
System.out.println("执行了有参数的 info 方法,参数为:" + str);
}
// 定义一个测试用的内部类
static class Inner {
}
public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException {
// 获取 ClassTest 对应的 Class 对象
Class<ClassTest> clazz = ClassTest.class;
// 获取该 Class 对象所对应类的全部构造器
Constructor<?>[] ctors = clazz.getDeclaredConstructors();
System.out.println("ClassTest 全部的构造器如下:");
for (Constructor<?> ctor : ctors) {
System.out.println(ctor);
}
// 获取该 Class 对象所对应类的全部 public 构造器
Constructor<?>[] publicCtors = clazz.getConstructors();
System.out.println("ClassTest 的全部 public 构造器如下:");
for (Constructor<?> publicCtor : publicCtors) {
System.out.println(publicCtor);
}
// 获取该 Class 对象所对应类的全部 public 方法
Method[] methods = clazz.getMethods();
System.out.println("ClassTest的全部 public 方法如下");
for (Method method : methods) {
System.out.println(method);
}
// 获取该 Class 对象所对应类的指定方法
System.out.println("ClassTest里带一个字符串参数的info方法为" + clazz.getMethod("info", String.class));
// 获取该 Class 对象所对应类的全部注解
Annotation[] annotations = clazz.getAnnotations();
System.out.println("ClasTest的全部注解如下");
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
System.out.println("该 Class 对象上的 @SuppressWarnings注解为:" + Arrays.toString(clazz.getAnnotationsByType(SuppressWarnings.class)));
System.out.println("该 Class 对象上的 @Anno注解:" + Arrays.toString(clazz.getAnnotationsByType(Anno.class)));
// 获取该 Class 对象所对应类的全部内部类
Class<?>[] inners = clazz.getDeclaredClasses();
System.out.println("ClassTest的全部内部类如下:");
for (Class<?> inner : inners) {
System.out.println(inner);
}
// 使用 Class.forName() 方法加载 ClassTest 的内部类
Class<?> inClazz = Class.forName("main.info.ClassTest$Inner");
System.out.println("inClazz对应类的外部类为:" + inClazz.getDeclaringClass());
System.out.println("ClassTest的包为:" + clazz.getPackage());
System.out.println("ClassTest的父类为:" + clazz.getSuperclass());
}
}
值得指出的是,虽然定义 ClassTest 类时使用了 @SuppressWarnings 注解,但程序运行时无法分析出该类里包含的该注解,这是因为 @SuppressWarnings 使用了 @Retention(value=SOURCE)修饰,这表明 @SuppressWarnings 只能保存在源代码级别上,而通过 ClassTest.class 获取该类的运行时 Class 对象,所以程序无法访问到 @SuppressWarnings 注解。对于只能在源代码上保留的注解,使用运行时获得的 Class 对象无法访问到该注解对象。
(三)、方法参数反射
Java8 在 java.lang.reflect 包下新增了一个 Executable 抽象基类,该对象代表可执行的类成员,该类派生了 Constructor、Method 两个子类。Executable 基类提供了大量方法来获取修饰该方法或构造器的注解信息;还提供了: isVarArgs() 方法用于判断该方法或构造器是否包含数量可变的形参,通过 getModifiers() 方法来获取该方法或构造器的修饰符。除此之外,Executable 提供了如下两个方法来获取该方法或参 数的形参个数及形参名。
➢ int getParameterCount():获取该构造器或方法的形参个数。
➢ Parameter[] getParameters():获取该构造器或方法的所有形参。
上面第二个方法返回了一个 Parameter[] 数组,Parameter 也是 Java8 新增的 API,每个 Parameter 对象代表方法或构造器的一个参数。Parameter 也提供了大量方法来获取声明该参数的泛型信息,还提供了如下常用方法来获取参数信息。
➢ getModifiers():获取修饰该形参的修饰符。
➢ String getName():获取形参名。
➢ Type getParameterizedType():获取带泛型的形参类型。
➢ Class<?> getType():获取形参类型。
➢ boolean isNamePresent():该方法返回该类的 class 文件中是否包含了方法的形参名信息。
➢ boolean isVarArgs():该方法用于判断该参数是否为个数可变的形参。
需要指出的是,使用 javac 命令编译 Java 源文件时,默认生成的 class 文件并不包含方法的形参名信息,因此调用 isNamePresent() 方法将会返回 false,调用getName() 方法也不能得到该参数的形参名。如果希望 javac 命令编译 Java 源文件时可以保留形参信息,则需要为该命令指定 -parameters 选项。
MethodParameterTest.java
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;
/**
* <p>
* 方法参数反射
* </p>
*
* @author 醉瑾
* @since 2022-07-08
*/
class Test {
public void replace(String str, List<String> list) {
}
}
public class MethodParameterTest {
public static void main(String[] args) throws NoSuchMethodException {
// 获取 String 的类
Class<Test> clazz = Test.class;
// 获取 String 类的带两个参数的 replace() 方法
Method replace = clazz.getMethod("replace", String.class, List.class);
// 获取指定方法的参数个数
System.out.println("replace方法参数个数:" + replace.getParameterCount());
// 获取 replace 的所有参数信息
Parameter[] parameters = replace.getParameters();
int index = 1;
for (Parameter parameter : parameters) {
if (parameter.isNamePresent()) {
System.out.println("======第" + index++ + "个参数信息======");
System.out.println("参数名:" + parameter.getName());
System.out.println("参数类型:" + parameter.getType());
System.out.println("泛型类型:" + parameter.getParameterizedType());
}
}
}
}
由于上面程序中 p.isNamePresent() 表示只有当该类的 class 文件中包含形参名信息时,程序才会执行条件体内的三行粗体字代码。因此需要使用如下命令来编译该程序:
javac -encoding UTF-8 -parameters -d . MethodParameterTest.java
上面命令中 -parameters 选项用于控制 javac 命令保留方法形参名信息。
执行命令
java MethodParameterTest
四、使用反射生成并操作对象
Class 对象可以获得该类里的方法(由Method对象表示)、构造器 (由Constructor对象表示)、成员变量(由Field对象表示),这三个类都位于 java.lang.reflect 包下,并实现了 java.lang.reflect.Member 接口。程序可以通过 Method 对象来执行对应的方法,通过 Constructor 对象来调用对应的构造器创建实例,能通过 Field 对象直接访问并修改对象的成员变量值。
(一)、创建对象
通过反射来生成对象需要先使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance() 方法来创建该 Class 对象对应类的实例。通过这种方式可以选择使用指定的构造器来创建实例。在很多 JavaEE 框架中都需要根据配置文件信息来创建Java对象,从配置文件读取的只是某个类的字符串类名,程序需要根据该字符串来创建对应的实例,就必须使用反射。
下面程序就实现了一个简单的对象池,该对象池会根据配置文件读取 key-value 对,然后创建这些对象,并将这些对象放入一个 HashMap 中。
设置配置文件 obj.txt
a=java.util.Date
b=javax.swing.JFrame
代码实现 ObjectPoolFactory.java
package main.info;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* <p>
*
* </p>
*
* @author 醉瑾
* @since 2022-07-08
*/
public class ObjectPoolFactory {
// 定义一个对象池,前面是对象名,后面是实际对象
private Map<String, Object> objectPool = new HashMap<>();
/*
* 定义一个创建对象的方法,该方法只要传入一个字符串类名,
* 程序可以根据该类名生成 Java 对象
* */
private Object createObject(String clazzName) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 根据字符串来获取对应的 Class 对象
Class<?> clazz = Class.forName(clazzName);
// 使用 clazz 对应类的默认构造器创建实例
return clazz.getConstructor().newInstance();
}
/*
* 该方法根据指定文件来初始化对象池
* 它会根据配置文件来创建对象
* */
public void initPool(String fileName) throws FileNotFoundException {
try (FileInputStream fileInputStream = new FileInputStream(fileName)) {
Properties props = new Properties();
props.load(fileInputStream);
for (String name : props.stringPropertyNames()) {
/*
* 读取出一对 key-value 队,根据 value 创建一个对象
* 调用 createObject() 创建对象,并将对象添加到对象池中
* */
objectPool.put(name, createObject(props.getProperty(name)));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Object getObject(String name) {
// 从 objectPool 中取出指定 name 对应的对象
return objectPool.get(name);
}
public static void main(String[] args) throws FileNotFoundException {
ObjectPoolFactory poolFactory = new ObjectPoolFactory();
poolFactory.initPool("D:\\IDEA_Project\\java-base\\src\\main\\info\\obj.txt");
System.out.println(poolFactory.getObject("a")); // ①
System.out.println(poolFactory.getObject("b")); // ②
}
}
上面程序中 createObject() 方法就是根据字符串来创建 Java 对象的关键代码,程序调用 Class 对象的 newInstance() 方法即可创建一个 Java 对象。程序中的 initPool() 方法会读取属性文件,对属性文件中每个 key-value 对创建一个Java对象,其中 value 是该 Java 对象的实现类,而 key 是该 Java 对象放入对象池中的名字。
编译、运行上面的 ObjectPoolFactory 程序,执行到 main 方法中的 ① 号代码处,将看到输出系统当前时间——这表明对象池中已经有了一个名为 a 的对象,该对象是一个 java.util.Date 对象。执行到 ② 号代码处,将看到输出一个 JFrame 对象。
如果不想利用默认构造器来创建 Java 对象,而想利用指定的构造器来创建 Java 对象,则需要利用 Constructor 对象,每个 Constructor 对应一个构造器。为了利用指定的构造器来创建 Java 对象,需要如下三个步骤。
① 获取该类的Class对象。
② 利用Class对象的getConstructor()方法来获取指定的构造器。
③ 调用Constructor的newInstance()方法来创建Java对象。
下面程序利用反射来创建一个 JFrame 对象,而且使用指定的构造器。
CreateJFrame.java
public class CreateJFrame {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 获取 JFrame 对应的 Class 对象
Class<?> jframeClazz = Class.forName("javax.swing.JFrame");
// 获取 JFrame 中带一个字符串参数的构造器
Constructor<?> ctor = jframeClazz.getConstructor(String.class); // ①
// 调用 Constructor 的 newInstance 对象
Object obj = ctor.newInstance("测试窗口"); // ②
System.out.println(obj);
}
}
上面程序中 ① 处代码用于获取 JFrame 类的指定构造器,前面已经提到:如果要唯一地确定某类中的构造器,只要指定构造器的形参列表即可。① 处代码获取构造器时传入了一个 String 类型,即表明想获取只有一个字符串参数的构造器。
程序中 ② 处代码使用指定构造器的 newInstance() 方法来创建一个 Java 对象,当调用 Constructor 对象的 newInstance() 方法时通常需要传入参数,因为调用 Constructor 的 newInstance() 方法实际上等于调用它对应的构造器,传给 newInstance() 方法的参数将作为对应构造器的参数。
对于上面的 CreateFrame.java 中已知 java.swing.JFrame 类的情形,通常没有必要使用反射来创建该对象,毕竟通过反射创建对象时性能要稍低一些。实际上,只有当程序需要动态创建某个类的对象时才会考虑使用反射,通常在开发通用性比较广的框架、基础平台时可能会大量使用反射。
(二)、调用方法
当获得某个类对应的 Class 对象后,就可以通过该 Class 对象的 getMethods() 方法或者 getMethod() 方法来获取全部方法或指定方法——这两个方法的返回值是 Method 数组,或者 Method 对象。
每个 Method 对象对应一个方法,获得 Method 对象后,程序就可通过该 Method 来调用它对应的方法。在 Method 里包含一个 invoke() 方法,该方法的签名如下。
➢ Object invoke(Object obj, Object…args)
该方法中的obj 是执行该方法的主调,后面的 args 是执行该方法时传入该方法的 实参。
下面程序对前面的对象池工厂进行加强,允许在配置文件中增加配置对象的成员变量的值,对象池工厂会读取为该对象配置的成员变量值,并利用该对象对应的 setter 方法设置成员变量的值。
ExtendedObjectPoolFactory.java
package main.info;
import java.util.*;
import java.io.*;
import java.lang.reflect.*;
public class ExtendedObjectPoolFactory {
// 定义一个对象池,前面是对象名,后面是实际对象
private Map<String, Object> objectPool = new HashMap<>();
private Properties config = new Properties();
// 从指定属性文件中初始化Properties对象
public void init(String fileName) {
try (
FileInputStream fis = new FileInputStream(fileName)) {
config.load(fis);
} catch (IOException ex) {
System.out.println("读取" + fileName + "异常");
}
}
// 定义一个创建对象的方法
// 该方法只要传入一个字符串类名,程序可以根据该类名生成Java对象
private Object createObject(String clazzName)
throws Exception {
// 根据字符串来获取对应的Class对象
Class<?> clazz = Class.forName(clazzName);
// 使用clazz对应类的默认构造器创建实例
return clazz.getConstructor().newInstance();
}
// 该方法根据指定文件来初始化对象池
// 它会根据配置文件来创建对象
public void initPool() throws Exception {
for (String name : config.stringPropertyNames()) {
// 每取出一个key-value对,如果key中不包含百分号(%)
// 这就表明是根据value来创建一个对象
// 调用createObject创建对象,并将对象添加到对象池中
if (!name.contains("%")) {
objectPool.put(name,
createObject(config.getProperty(name)));
}
}
}
// 该方法将会根据属性文件来调用指定对象的setter方法
public void initProperty() throws InvocationTargetException
, IllegalAccessException, NoSuchMethodException {
for (String name : config.stringPropertyNames()) {
// 每取出一对key-value对,如果key中包含百分号(%)
// 即可认为该key用于控制调用对象的setter方法设置值
// %前半为对象名字,后半控制setter方法名
if (name.contains("%")) {
// 将配置文件中的key按%分割
String[] objAndProp = name.split("%");
// 取出调用setter方法的参数值
Object target = getObject(objAndProp[0]);
// 获取setter方法名:set + "首字母大写" + 剩下部分
String mtdName = "set" +
objAndProp[1].substring(0, 1).toUpperCase()
+ objAndProp[1].substring(1);
// 通过target的getClass()获取它的实现类所对应的Class对象
Class<?> targetClass = target.getClass();
// 获取希望调用的setter方法
Method mtd = targetClass.getMethod(mtdName, String.class);
// 通过Method的invoke方法执行setter方法
// 将config.getProperty(name)的值作为调用setter方法的参数
mtd.invoke(target, config.getProperty(name));
}
}
}
public Object getObject(String name) {
// 从objectPool中取出指定name对应的对象
return objectPool.get(name);
}
public static void main(String[] args)
throws Exception {
ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory();
epf.init("D:\\IDEA_Project\\java-base\\src\\main\\info\\extObj.txt");
epf.initPool();
epf.initProperty();
System.out.println(epf.getObject("a"));
}
}
配置文件 extObj.txt
a=javax.swing.JFrame
b=javax.swing.JLabel
#set the title of a
a%title=Test Title
面配置文件中的 a%title 行表明希望调用 a 对象的 setTitle() 方法,调用该方法的参数值为 Test Title 。编译、运行上面的 ExtendedObjectPoolFactory.java 程序,可以看到输出一个 JFrame 窗口,该窗口的标题为 Test Title。
扩展
Spring 框架就是通过这种方式将成员变量值以及依赖对象等都放在配置文件中进行管理的,从而实现了较好的解耦。这也是 Spring 框架的IoC的秘密。
当通过 Method 的 invoke() 方法来调用对应的方法时,Java 会要求程序必须有调用该方法的权限。如果程序确实需要调用某个对象的 private 方法,则可以先调用Method对象的如下方法。
➢ setAccessible(boolean flag)
将 Method 对象的 accessible 设置为指定的布尔值。值为 true,指示该 Method 在使用时应该取消 Java 语言的访问权限检查;值为false,则指示该 Method 在使用时要实施 Java 语言的访问权限检查。
说明
实际上,setAccessible() 方法并不属于 Method,而是属于它的父类 AccessibleObject。因此Method、Constructor、Field 都可调用该方法,从而实现通过反射来调用 private 方法、private 构造器和 private 成员变量。也就是说,它们可以通过调用该方法来取消访问权限检查,通过反射即可访问
private 成员。
(三)、访问成员变量值
通过 Class 对象的 getFields() 或 getField() 方法可以获取该类所包括的全部成员变量或指定成员变量。Field 提供了如下两组方法来读取或设置成员变量值。
➢ getXxx(Object obj)
获取obj对象的该成员变量的值。此处的 Xxx对应 8 种基本类型,如果该成员变量的类型是引用类型,则取 消 get 后面的 Xxx。
➢ setXxx(Object obj, Xxx val)
将 obj 对象的该成员变量设置成 val 值。此处的 Xxx 对应8种基本类型,如果该成员变量的类型 是引用类型,则取消 set 后面的 Xxx。
使用这两个方法可以随意地访问指定对象的所有成员变量,包括 private 修饰的成员变量。
下面是一个测试程序 FieldTest.java
package main.field;
import java.io.File;
import java.lang.reflect.Field;
/**
* <p>
*
* </p>
*
* @author 醉瑾
* @since 2022-07-09
*/
class Person {
private String name;
private int age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class FieldTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 创建一个 Person 对象
Person person = new Person();
// 获取 Person 类对应的 class 对象
Class<Person> personClazz = Person.class;
// 获取 Person 的名为 name 的成员变量
// 使用 getDeclareField() 方法表示可以获取各种访问控制符的成员变量
Field name = personClazz.getDeclaredField("name");
// 通过设置反射访问该成员变量时取消访问权限检查
name.setAccessible(true);
// 调用 set 方法为 person 对象的 name 成员变量设置值
name.set(person, "zuijin");
// 获取 Person 类名为 age 的成员变量
Field age = personClazz.getDeclaredField("age");
age.setAccessible(true);
// age.set(person, 18);
age.setInt(person, 18);
System.out.println(person);
}
}
上面程序中先定义了一个 Person 类,该类里包含两个 private 成员变量:name 和 age,在通常情况下,这两个成员变量只能在 Person 类里访问。但本程序 FieldTest 的 main() 方法中通过setAccessible(true) 反射修改了 Person 对象的 name、age 两个成员变量的值。
输出
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7ZRrSTa-1658155692092)(http://note.lovewj.top/imageimage-20220709100934393.png)]
(四)、操作数组
在 java.lang.reflect 包下还提供了一个 Array 类,Array 对象可以代表所有的数组。程序可以通过使用 Array 来动态地创建数组,操作数组元素等。Array 提供了如下几类方法。
➢ static Object newInstance(Class<?>componentType,int…length)
创建一个具有指定的元素类型、指定维度的新数组。
➢ static xxx getXxx(Object array, int index)
返回 array 数组中第 index 个元素。其中 xxx 是各种基本数据类型,如果数组元素是引用类型,则该方法变为 get(Object array , int index)。
➢ static void setXxx(Object array, int index, xxx val)
将 array 数组中第 index 个元素的值设为 val。其中 xxx 是各种基本数据类型,如果数组元素是引用类型,则该方法变成 set(Object array,int index,Object val)。
下面程序示范了如何使用 Array 来生成数组,为指定数组元素赋值,并获取指定数组元素的方式。
ArrayTest1.java
public class ArrayTest1 {
public static void main(String[] args) {
try {
// 创建一个元素类型为 String,长度为 10 的数组
Object arr = Array.newInstance(String.class, 10);
// 依次为 arr 数组中 index 为 5,6 的元素赋值
Array.set(arr, 5, "醉瑾1号");
Array.set(arr, 6, "醉瑾2号");
// 依次取出 arr 数组中 index 为 5,6 的元素
Object k1 = Array.get(arr, 5);
Object k2 = Array.get(arr, 6);
// 输出 arr 数组中 index 为 5,6 的元素
System.out.println(k1);
System.out.println(k2);
} catch (Throwable e) {
System.err.println(e);
}
}
}
创建一个三维数组 ArrayTest2.java
public class ArrayTest2 {
public static void main(String[] args) {
/*
* 创建一个三维数组
* 三维数组也是一维数组,是数组元素是二维数组的一维数组
* */
Object arr = Array.newInstance(String.class, 3, 4, 10);
// 获取 arr 数组中 index 为 2 的元素,该元素是二维数组
Object o = Array.get(arr, 2);
// 使用 Array 为二维数组的数组元素赋值,二维数组的数组元素是一维数组
// 所以传入 Array 的 set() 方法的第三个参数是一维数组
Array.set(o, 2, new String[]{"你好1号", "你好2号"});
// 获取 o 数组中 index 为 3 的元素,该元素应该是一维数组
Object anArr = Array.get(o, 3);
Array.set(anArr, 8, "你好dd");
// 将 arr 强制转为三维数组
var cast = (String[][][]) arr;
System.out.println(Arrays.deepToString(cast));
System.out.println(cast[2][3][8]);
System.out.println(cast[2][2][0]);
System.out.println(cast[2][2][1]);
}
}
(五)、Java11 新增的嵌套访问权限
在 Java 11 之前,外部类可以访问内部类的 private 成员,内部类之间也可相互访问对方的 private 成员;但如果外部类通过反射访问内部类的 private 成员,或者内部类之间通过反射访问对方的 private 成员,这就不行了。看如下程序。
NestTest.java
package main.field;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* <p>
*
* </p>
*
* @author 醉瑾
* @since 2022-07-09
*/
public class NestTest {
public class InA {
private int age = 18;
private void foo() {
System.out.println("InA private 的 foo 方法");
}
}
public class InB {
private String name = "醉瑾";
private void bar() throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
InA a = new InA();
// 访问另一个内部类的 private 成员
a.age = 20;
System.out.println(a.age);
a.foo();
System.out.println("InB private 的 bar 方法");
// 通过反射访问另一个内部类的 private 成员
// 在 java 11 之前报错,在 java 11 之后没有问题
Field age = InA.class.getDeclaredField("age");
age.set(a, 10); // ①
System.out.println(age.get(a)); // ②
Method foo = InA.class.getDeclaredMethod("foo");
foo.invoke(a); // ③
}
}
public void info() throws NoSuchFieldException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
InB b = new InB();
// 外部类访问内部类的 private 成员
b.name = "zuijin";
System.out.println(b.name);
b.bar();
Field name = InB.class.getDeclaredField("name");
name.set(b, "ZUIJIN"); // ④
System.out.println(name.get(b)); // ⑤
Method bar = InB.class.getDeclaredMethod("bar");
bar.invoke(b); // ⑥
}
public static void main(String[] args) throws NoSuchFieldException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
new NestTest().info();
}
}
如果使用 Java 11 之前的 JDK 编译、运行上面程序,将会看到程序在 ① 号代码处引发异常,这说明内部类 B 不能通过反射访问内部类 A 的 private 成员;后面 ②③④⑤⑥ 号代码同样会引发这个异常。但是在 ① 号代码之前,内部类B可以(不使用反射)访问内部类 A 的 private 成员,这就是 Java 11 之前存在的问题:通过反射访问和不通过反射访问时,Java 的访问权限并不一致。为了解决这个问题,Java 11 引入了嵌套上下文的概念。通过嵌套访问权限的支持,Java 11 统一了通过反射访问和不通过反射访问时的权限不一致的问题。因此,如果使用 Java 11 编译、运行上面程序,将可以看到程序可以成功运行。
与之对应的是,Java 11为Class类新增了如下方法。
➢ Class<?> getNestHost():返回该类所属的嵌套属主。
➢ boolean isNestmateOf(Class<?> c):判断该类是否为 c 的嵌套同伴(nestmate),只要两个类有相同的嵌套属主,它们就是嵌套同伴。
➢ Class<?>[] getNestMembers():获取该类的所有嵌套成员。
在上面 NestTest.java main方法中添加如下代码
public static void main(String[] args) throws NoSuchFieldException, InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException {
new NestTest().info();
System.out.println("=================================");
//获取NestTest的嵌套属主类,由于它自身就是外部类,因此返回它自身(NestTest)
System.out.println(NestTest.class.getNestHost());
//获取NestTest.InA的嵌套属主类,返回NestTest类
System.out.println(Class.forName("main.field.NestTest$InA").getNestHost());
//获取NestTest的所有嵌套成员,将会看到NestTest、 InA、 InB三个嵌套成员
System.out.println(Arrays.toString(NestTest.class.getNestMembers()));
//判断NestTest. InA是否为NestTest. InB的嵌套同伴,返回true
System.out.println(Class.forName("main.field.NestTest$InA").isNestmateOf(Class.forName("main.field.NestTest$InB")));
}
将会看到如下输出
......
=================================
class main.field.NestTest
class main.field.NestTest
[class main.field.NestTest, class main.field.NestTest$InB, class main.field.NestTest$InA]
true
通过上面输出可以看出,对于外部类而言,它的嵌套属主就是它自身;对于内部类而言,它的嵌套属主就是它所在的外部类。
五、使用反射生成 JDK 动态代理
在 Java 的 java.lang.reflect 包下提供了一个 Proxy 类和一个 InvocationHandler 接口,通过使用这个类和接口可以生成JDK动态代理类或动态代理对象。
(一)、使用 Proxy 和 InvocationHandler 创建动态代理
Proxy 提供了用于创建动态代理类和代理对象的静态方法,它也是所有动态代理类的父类。如果在程序中为一个或多个接口动态地生成实 现类,就可以使用 Proxy 来创建动态代理类;如果需要为一个或多个接
口动态地创建实例,也可以使用 Proxy 来创建动态代理实例。
Proxy提供了如下两个方法来创建动态代理类和动态代理实例。
➢ static Class<?> getProxyClass(ClassLoader loader, Class<?>…interfaces)
创建一个动态代理类所对应的 Class 对象,该代理类将实现 interfaces 所指定的多个接口。第一个 ClassLoader 参数指定生成动态代理类的类加载器。
➢ static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
直接创建一个动态代理对象,该代理对象的实现类实现了 interfaces 指定的系列接口,执行代理对象的每个方法时都会被替换执行 InvocationHandler 对象的 invoke 方法。
实际上,即使采用第一个方法生成动态代理类之后,如果程序需要通过该代理类来创建对象,依然需要传入一个 InvocationHandler 对象。也就是说,系统生成的每个代理对象都有一个与之关联的 InvocationHandler 对象。
提示
当程序使用反射方式为指定接口生成系列动态代理对象时,这些动态代理对象的实现类实现了一个或多个接口。动态代理对象就需要实现一个或多个接口里定义的所有方法,但问题是:系统怎么知道如何实现这些方法?这个时候就轮到 InvocationHandler 对象登场了——当执行动态代理对象里的方法时,实际上会替换成调用 InvocationHandler 对象的 invoke 方法。
下面为 Proxy 和 InvocationHandler 生成动态代理对象的实例 ProxyTest.java
package main.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* <p>
*
* </p>
*
* @author 醉瑾
* @since 2022-07-09
*/
interface Person {
void walk();
void sayHello(String name);
}
class MyInvocationHandler implements InvocationHandler {
/*
* 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
* 其中
* proxy:代表动态代理对象
* method:代表正在执行的方法
* args:代表调用目标方法时传入的实参
* */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("------正在执行的方法:" + method);
if (args != null) {
System.out.println("下面是执行该方法时传入的实参:");
for (Object arg : args) {
System.out.println(arg);
}
} else {
System.out.println("调用方法没有参数");
}
return null;
}
}
public class ProxyTest {
public static void main(String[] args) {
// 创建一个 InvocationHandler 对象
InvocationHandler handler = new MyInvocationHandler();
// 使用指定的 InvocationHandler 来生成一个动态代理对象
Person person = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{Person.class}, handler);
// 调用动态代理对象的 walk() 和 sayHello() 方法
person.walk();
person.sayHello("醉瑾");
}
}
运行结果
------正在执行的方法:public abstract void main.proxy.Person.walk()
调用方法没有参数
------正在执行的方法:public abstract void main.proxy.Person.sayHello(java.lang.String)
下面是执行该方法时传入的实参:
醉瑾
(二)、动态代理和 AOP
根据前面介绍的 Proxy 和 InvocationHandler,实在很难看出这种动态代理的优势。下面介绍一种更实用的动态代理机制。 开发实际应用的软件系统时,通常会存在相同代码段重复出现的情况,在这种情况下,对于许多刚开始从事软件开发的人而言,他们的做法是:选中那些代码,一路“复制”“粘贴”,立即实现了系统功能,如果仅仅从软件功能上来看,他们确实已经完成了软件开发。如图
在软件开发期间可能会觉得无所谓,但如果有一天需要修改程序的深色代码的实现,则意味着打开三份源代码进行修改。如果有 100 个地方甚至 1000 个地方使用了这段深色代码段,那么修改、维护这段代码的工作量将变成噩梦。
在这种情况下,大部分稍有经验的开发者都会将这段深色代码段定 义成一个方法,然后让另外三段代码段直接调用该方法即可。在这种方式下,软件系统的结构如图
如果需要修改深色部分的代码,则 只要修改一个地方即可,而调用该方法的代码段,不管有多少个地方调用了该方法,都完全无须任何修改,只要被调用方法被修改了,所有调 用该方法的地方就会自然改变——通过这种方式,大大降低了软件后期维护的复杂度。
但采用这种方式来实现代码复用依然产生一个重要问题:代码段 1、代码段2、代码段3和深色代码段分离开了,但代码段1、代码段2和 代码段3又和一个特定方法耦合了!最理想的效果是:代码段1、代码段 2和代码段3既可以执行深色代码部分,又无须在程序中以硬编码方式直 接调用深色代码的方法,这时就可以通过动态代理来达到这种效果。 由于 JDK 动态代理只能为接口创建动态代理
,所以下面先提供一个 Dog 接口,该接口代码非常简单,仅仅在该接口里定义了两个方法。
public interface Dog {
void info();
void run();
}
上面接口里只是简单地定义了两个方法,并未提供方法实现。如果直接使用Proxy为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果又将完全一样。实际情况通常是,软件系统会为该Dog接口提供一个或多个实现类。此处先提供一个简单的实现类:GunDog。
public class GunDog implements Dog {
@Override
public void info() {
System.out.println("一只猎狗");
}
@Override
public void run() {
System.out.println("速度七十迈");
}
}
上面代码没有丝毫的特别之处,该Dog的实现类仅仅为每个方法提供了一个简单实现。
再看需要实现的功能:让代码段1、代码段2和代码 段3既可以执行深色代码部分,又无须在程序中以硬编码方式直接调用 深色代码的方法。此处假设info()、run()两个方法代表代码段1、代码段2,那么要求:程序执行 info()、run() 方法时能调用某个通用方法,但又不想以硬编码方式调用该方法。下面提供一个DogUtil类,该类里包含两个通用方法。
public class DogUtil {
// 第一个拦截器
public void method1() {
System.out.println("========模拟第一个通用方法========");
}
// 第二个拦截器
public void method2() {
System.out.println("=========模拟第二个通用方法=========");
}
}
借助于 Proxy 和 InvocationHandler 就可以实现——当程序调用 info() 方法和 run() 方法时,系统可以“自动”将 method1() 和 method2() 两个通用方法插入 info() 和 run() 方法中执行。
这个程序的关键在于下面的 MyInvokationHandler 类,该类是一个 InvocationHandler 实现类,该实现类的 invoke() 方法将会作为代理对象的方法实现。
public class MyInvocationHandler implements InvocationHandler {
// 需要被代理的对象
private Object target;
public void setTarget(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
DogUtil dogUtil = new DogUtil();
// 执行 DogUtil 对象中的 method1 方法
dogUtil.method1();
// 以 target 作为主调来执行 method 方法
Object result = method.invoke(target, args);
// 执行 DogUtil 对象中的 method2 方法
dogUtil.method2();
return result;
}
}
上面程序实现 invoke() 方法时包含了一行关键代码Object result = method.invoke(target, args)
,这行代码通过反射以 target 作为主调来执行 method 方法,这就是回调了 targe t对象的原有方法。在粗体字代码之前调用 DogUtil 对象的 method1() 方法,在粗体字代码之后调用 DogUtil 对象的method2()方法。
下面再为程序提供一个 MyProxyFactory 类,该对象专为指定的 target生成动态代理实例。
public class MyProxyFactory {
// 为指定的 target 生成动态代理对象
public static Object getProxy(Object target){
// 创建一个 MyInvocationHandler 对象
MyInvocationHandler handler = new MyInvocationHandler();
// 为 MyInvocationHandler 设置 target 对象
handler.setTarget(target);
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),handler);
}
}
上面的动态代理工厂类提供了一个 getProxy() 方法,该方法为 target 对象生成一个动态代理对象,这个动态代理对象与 target 实现了相同的接口,所以具有相同的 public 方法——从这个意义上来看,动态 代理对象可以当成 target 对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MyInvokationHandler 对象的 invoke() 方法。例如,调用动态代理对象的info()方法,程序将开始执行 invoke() 方法,其执行步骤如下。
① 创建 DogUtil 实例。
② 执行 DogUtil 实例的 method1() 方法。
③ 使用反射以 target 作为调用者执行 info() 方法。
④ 执行 DogUtil 实例的 method2() 方法。
看到上面的执行过程,读者应该已经发现:当使用动态代理对象来代替 target 对象时,代理对象的方法就实现了前面的要求——程序执行 info()、run()方法时既能“插入” method1()、method2() 通用方法,但 GunDog 的方法中又没有以硬编码方式调用 method1() 和 method2() 方法。
测试程序、
public class Test {
public static void main(String[] args) {
// 创建一个原始的 GunDog 对象,作为 target
Dog target = new GunDog();
// 以指定的 target 来创建动态代理对象
Dog dog = (Dog) MyProxyFactory.getProxy(target);
dog.info();
dog.run();
}
}
上面程序中的 dog 对象实际上是动态代理对象,只是该动态代理对象也实现了 Dog 接口,所以也可以当成Dog 对象使用。程序执行 dog 的 info() 和 run() 方法时,实际上会先执行 DogUtil 的 method1() 方法,再执行 target 对象的 info() 和 run() 方法,最后执行 DogUtil 的 method2() 方法。
采用动态代理可以非常灵活地实现解耦。通常而言,使用 Proxy 生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的实际意义。通常都是为指定的目标对象生成动态代理。
这种动态代理在 AOP(Aspect Orient Programming,面向切面编程)中被称为 AOP 代理,AOP 代理可代替目标对象,AOP 代理包含了目标对象的全部方法。但 AOP 代理中的方法与目标对象的方法存在差异:AOP 代理里的方法可以在执行目标方法之前、之后插入一些通用处理。
AOP代理包含的方法与目标对象包含的方法示意图如图
六、反射和泛型
从JDK 5以后,Java 的 Class 类增加了泛型功能,从而允许使用泛型来限制 Class 类,例如, String.class 的类型实际上是 Class<String>。 如果 Class 对应的类暂时未知,则使用Class<?>。通过在反射中使用泛型,可以避免使用反射生成的对象需要强制类型转换。
(一)、泛型和 Class 类
使用Class<T>泛型可以避免强制类型转换。例如,下面提供一个简单的对象工厂,该对象工厂可以根据指定类来提供该类的实例。
CrazyObjectFactory.java
public class CrazyObjectFactory {
public static Object getInstance(String className) {
try {
// 创建指定类对应的 Class 对象
Class<?> clazz = Class.forName(className);
// 返回使用该 Class 对象创建的实例
return clazz.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
上面程序中两行粗体字代码根据指定的字符串类型创建了一个新对象,但这个对象的类型是 Object,因此当 需要使用 CrazyitObjectFactory 的 getInstance() 方法来创建对象时需要进行强制转换。在强制转换时如:
var f = (JFrame) Crazyit.getInstance ("java.util.Date");
或
var d = (Date) Crazyit . getInstance ("java.util.Date");
上面代码在编译时不会有任何问题,但运行时将抛出 ClassCastException异常,因为程序试图将一个Date对象转换成 JFrame 对象。
如果将上面的 CrazyObjectFactory 工厂类改写成使用泛型后的 Class,就可以避免这种情况。
public class CrazyObjectFactory2 {
public static <T> T getInstance(Class<T> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
return clazz.getConstructor().newInstance();
}
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
// 获取实例后无须类型转换
Date instance = CrazyObjectFactory2.getInstance(Date.class);
JFrame jFrame = CrazyObjectFactory2.getInstance(JFrame.class);
}
}
在上面程序的 getInstance() 方法中传入一个 Class<T>参数,这是一个泛型化的 Class 对象,调用该 Class 对象的 newInstance() 方法将返 回一个 T 对象,如程序中粗体字代码所示。接下来当使用 CrazyObjectFactory2 工厂类的 getInstance() 方法来产生对象时,无须使用强制类型转换,系统会执行更严格的检查,不会出现 ClassCastException 运行时异常。
(二)、使用反射来获取泛型信息
通过指定类对应的 Class 对象,可以获得该类里包含的所有成员变量,不管该成员变量是使用 private 修饰,还是使用 public 修饰。获得了成员变量对应的 Field 对象后,就可以很容易地获得该成员变量的数 据类型,即使用如下代码即可获得指定成员变量的类型。
Class<?> type=f.getType();
但这种方式只对普通类型的成员变量有效。如果该成员变量的类型 是有泛型类型的类型,如Map<String,Integer> 类型,则不能准确地得到该成员变量的泛型参数。为了获得指定成员变量的泛型类型,应先使用如下方法来获取该成员变量的泛型类型。
Type type=f.getGenericType();
然后将 Type 对象强制类型转换为 ParameterizedType 对象, ParameterizedType 代表被参数化的类型,也就是增加了泛型限制的类型。ParameterizedType 类提供了如下两个方法。
➢ getRawType():返回没有泛型信息的原始类型。
➢ getActualTypeArguments():返回泛型参数的类型。
下面是一个获取泛型类型的完整程序。
GenericTest.java
public class GenericTest {
private Map<String, Integer> score;
public static void main(String[] args) throws NoSuchFieldException {
Class<GenericTest> clazz = GenericTest.class;
Field field = clazz.getDeclaredField("score");
// 直接使用 getType() 取出类型只对普通类型的成员变量有效
Class<?> a = field.getType();
// 下面的输出将看到仅输出 java.util.Map
System.out.println("score的类型是:" + a);
// 获取成员变量的泛型类型
Type type = field.getGenericType();
// 如果 type 类型是 ParameterizedType 对象
if (type instanceof ParameterizedType) {
// 强制类型转换
ParameterizedType type1 = (ParameterizedType) type;
// 获取原始类型
Type type2 = type1.getRawType();
System.out.println("原始类型是:" + type2);
// 取得泛型类型的泛型参数
Type[] types = type1.getActualTypeArguments();
System.out.println("泛型参数信息是:");
for (Type t : types) {
System.out.println(t);
}
} else {
System.out.println("获取泛型参数类型出错");
}
}
}
运行输出
score的类型是:interface java.util.Map
原始类型是:interface java.util.Map
泛型参数信息是:
class java.lang.String
class java.lang.Integer
从上面的运行结果可以看出,使用 getType() 方法只能获取普通类 型的成员变量的数据类型;对于增加了泛型的成员变量,应该使用 getGenericType() 方法来取得其类型。
提示
Type 也是 java.lang.reflect 包下的一个接口,该接口代表所有类型的公共高级接口,Class 是Type 接口的实现类。Type包括原始类型、参数化类型、数组类型、类型变量和基本类型等。
ass<?> type=f.getType();
但这种方式只对普通类型的成员变量有效。如果该成员变量的类型 是有泛型类型的类型,如Map<String,Integer> 类型,则不能准确地得到该成员变量的泛型参数。为了获得指定成员变量的泛型类型,应先使用如下方法来获取该成员变量的泛型类型。
```java
Type type=f.getGenericType();
然后将 Type 对象强制类型转换为 ParameterizedType 对象, ParameterizedType 代表被参数化的类型,也就是增加了泛型限制的类型。ParameterizedType 类提供了如下两个方法。
➢ getRawType():返回没有泛型信息的原始类型。
➢ getActualTypeArguments():返回泛型参数的类型。
下面是一个获取泛型类型的完整程序。
GenericTest.java
public class GenericTest {
private Map<String, Integer> score;
public static void main(String[] args) throws NoSuchFieldException {
Class<GenericTest> clazz = GenericTest.class;
Field field = clazz.getDeclaredField("score");
// 直接使用 getType() 取出类型只对普通类型的成员变量有效
Class<?> a = field.getType();
// 下面的输出将看到仅输出 java.util.Map
System.out.println("score的类型是:" + a);
// 获取成员变量的泛型类型
Type type = field.getGenericType();
// 如果 type 类型是 ParameterizedType 对象
if (type instanceof ParameterizedType) {
// 强制类型转换
ParameterizedType type1 = (ParameterizedType) type;
// 获取原始类型
Type type2 = type1.getRawType();
System.out.println("原始类型是:" + type2);
// 取得泛型类型的泛型参数
Type[] types = type1.getActualTypeArguments();
System.out.println("泛型参数信息是:");
for (Type t : types) {
System.out.println(t);
}
} else {
System.out.println("获取泛型参数类型出错");
}
}
}
运行输出
score的类型是:interface java.util.Map
原始类型是:interface java.util.Map
泛型参数信息是:
class java.lang.String
class java.lang.Integer
从上面的运行结果可以看出,使用 getType() 方法只能获取普通类 型的成员变量的数据类型;对于增加了泛型的成员变量,应该使用 getGenericType() 方法来取得其类型。
提示
Type 也是 java.lang.reflect 包下的一个接口,该接口代表所有类型的公共高级接口,Class 是Type 接口的实现类。Type包括原始类型、参数化类型、数组类型、类型变量和基本类型等。