19. 类加载机制与反射
19. 类加载机制与反射
类的加载、连接和初始化
系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类。
JVM 和类
当我们调用 Java 命令运行某个 Java 程序时,该命令将会启动一个 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程里。
同一个 JVM 的所有线程、所有变量都处于同一个进程里,它们都使用该 JVM 进程的内存区。
当系统出现以下几种情况时,JVM 进程将被终止。
- 程序运行到最后正常结束。
- 程序运行到使用
System.exit()
或Runtime.getRuntime().exit()
代码处结束程序。 - 程序执行过程中遇到未捕获的异常或错误而结束。
- 程序所在平台强制结束了 JVM 进程。
JVM 进程结束,该进程在内存中的状态将会丢失。
public class A {
// 定义该类的静态Field
public static int a = 6;
}
定义一个类创建A
类的实例,并访问A
对象的a Field
。
public class ATest1 {
public static void main(String[] args) {
// 创建A类的实例
A a = new A();
// 让a实例的a Field值自加
A.a++;
System.out.println(A.a);
}
}
下面程序也创建A
对象,并访问其a Field
值。
public class ATest2 {
public static void main(String[] args) {
// 创建A类的实例
A b = new A();
// 输出b实例的a Field值
System.out.println(A.a);
}
}
ATest1
结果输出 7,但ATest2
结果输出 6。运行ATest1
和ATest2
是两次运行 JVM 进程,第一次运行 JVM 结束后,它对A
类所做的修改将全部丢失——第二次运行 JVM 时将再次初始化A
类。
两个 JVM 之间并不会共享数据。
类的加载
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化 3 个步骤来对该类进行初始化。
如果没有意外,JVM 将会连续完成这 3 个步骤,所以有时也把这 3 个步骤统称为类加载或类初始化。
类加载指的是将类的 class 文件读入内存,并为之创建一个java.lang.Class
对象,当程序中使用任何类时,系统都会为之建立一个java.lang.Class
对象。
类的加载由类加载器完成,类加载器通常由 JVM 提供,这些类加载器也是前面所有程序运行的基础,JVM 提供的这些类加载器通常被称为系统类加载器。
除此之外,还可以通过继承ClassLoader
基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
- 从本地文件系统加载 class 文件。
- 从 JAR 包加载 class 文件。
- 通过网络加载 class 文件。
- 把一个 Java 源文件动态编译,并执行加载。
类加载器通常无须等到“首次使用”该类时才加载该类,Java 虚拟机规范允许系统预先加载某些类。
类的连接
当类被加载之后,系统为之生成一个对应的 Class 对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到 JRE 中。
类连接又可分为如下 3 个阶段。
- 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
- 准备:类准备阶段则负责为类的静态
Field
分配内存,并设置默认初始值。 - 解析:将类的二进制数据中的符号引用替换成直接引用。
类的初始化
在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对静态Field
进行初始化。
在 Java 类中对静态Field
指定初始值有两种方式:
- 声明静态
Field
时指定初始值; - 使用静态初始化块为静态
Field
指定初始值。
public class Test {
// 声明a变量时指定初始值
static int a = 5;
static int b;
static int c;
static {
// 使用静态初始化块为b变量指定初始值
b = 6;
}
}
静态Field c
则没有指定初始值,它将采用默认初始值 0。
声明变量时指定初始值,静态初始化块都将被当成类的初始化语句,JVM 会按这些语句在程序中的排列顺序依次执行它们。
public class Test {
static {
// 使用静态初始化块为b变量指定初始值
b = 6;
System.out.println("----------");
}
// 声明a变量时指定初始值
static int a = 5;
static int b = 9; // ①
static int c;
public static void main(String[] args) {
System.out.println(Test.b);
}
}
当Test
类初始化结束后,该类的静态Field b
的值为 9。
JVM 初始化一个类包含如下几个步骤。
- 假如这个类还没有被加载和连接,则程序先加载并连接该类。
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
- 假如类中有初始化语句,则系统依次执行这些初始化语句。
JVM 最先初始化的总是java.lang.Object
类。当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。
类初始化的时机
当 Java 程序首次通过下面 6 种方式来使用某个类或接口时,系统就会初始化该类或接口。
创建类的实例。为某个类创建实例的方式包括:使用
new
操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。调用某个类的静态方法。
访问某个类或接口的静态
Field
,或为该静态Field
赋值。使用反射方式来强制创建某个类或接口对应的
java.lang.Class
对象。例如代码:
Class.forName("Person")
,如果系统还未初始化Person
类,则这行代码将会导致该Person
类被初始化,并返回Person
类对应的java.lang.Class
对象。初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
直接使用
java.exe
命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。
除此之外,下面的几种情形需要特别指出。
对于一个final
型的静态Field
,如果该Field
的值在编译时就可以确定下来,那么这个Field
相当于“宏变量”。Java 编译器会在编译时直接把这个Field
出现的地方替换成它的值,因此即使程序使用该静态Field
,也不会导致该类的初始化。
class MyTest {
// 使用一个字符串直接量为static final Field赋值
static final String compileConstant = "疯狂Java讲义";
static {
// 只在第一次初始化的时候运行
System.out.println("静态初始化块...");
}
}
public class CompileConstantTest {
public static void main(String[] args) {
// 访问、输出MyTest中的compileConstant Field
System.out.println(MyTest.compileConstant); //①
}
}
当某个静态Field
使用了final
修饰,而且它的值可以在编译时就确定下来,那么程序其他地方使用该静态Field
时,实际上并没有使用该静态Field
,而是相当于使用常量。
反之,如果final
类型的静态Field
的值不能在编译时确定下来,则必须等到运行时才可以确定该Field
的值,如果通过该类来访问它的静态Field
,则会导致该类被初始化。
class MyTest {
// 采用系统当前时间为static final Field赋值
static final String compileConstant = System.currentTimeMillis() + "";
static {
// 每次都会运行
System.out.println("静态初始化块...");
}
}
public class CompileConstantTest {
public static void main(String[] args) {
// 访问、输出MyTest中的compileConstant Field
System.out.println(MyTest.compileConstant); //①
}
}
当使用ClassLoader
类的loadClass()
方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class
的forName()
静态方法才会导致强制初始化该类。
package classLoading;
/**
* @author 空~ 2022/8/22 周一 16:21
**/
public class Tester {
static {
System.out.println("Tester类的静态初始化块...");
}
}
强制初始化。
package classLoading;
/**
* @author 空~ 2022/8/22 周一 16:18
**/
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader cl = ClassLoader.getSystemClassLoader();
// 下面语句仅仅是加载Tester类
cl.loadClass("classLoading.Tester");
System.out.println("系统加载Tester类");
// 下面语句才会初始化Tester类
Class.forName("classLoading.Tester");
}
}
结果:
系统加载 Tester 类 Tester 类的静态初始化块...
从上面运行结果可以看出,必须等到执行Class.forName("Tester")
时才完成对Tester
类的初始化。
类加载器
类加载器负责将.class 文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class
对象。
类加载器简介
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class
实例。一旦一个类被载入 JVM 中,同一个类就不会被再次载入了。
在 Java 中,一个类用其全限定类名(包括包名和类名)作为标识;但在 JVM 中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg
的包中有一个名为Person
的类,被类加载器ClassLoader
的实例kl
负责加载,则该Person
类对应的Class
对象在 JVM 中表示为(Person
、pg
、kl
)。
这意味着两个类加载器加载的同名类:(Person
、pg
、kl
)和(Person
、pg
、kl2
)是不同的、它们所加载的类也是完全不同、互不兼容的。
当 JVM 启动时,会形成由 3 个类加载器组成的初始类加载器层次结构。
Bootstrap ClassLoader
:根类加载器。Extension ClassLoader
:扩展类加载器。System ClassLoader
:系统类加载器。
Bootstrap ClassLoader
被称为引导(也称为原始或根)类加载器,它负责加载 Java 的核心类。在 Sun 的 JVM 中,当执行java.exe
命令时,使用-Xbootclasspath
选项或使用-D
选项指定sun.boot.class.path
系统属性值可以指定加载附加的类。
根类加载器非常特殊,它并不是java.lang.ClassLoader
的子类,而是由 JVM 自身实现的。
public class BootstrapTest {
public static void main(String[] args) {
// 获取根类加载器所加载的全部URL数组
URL[] urls = sun.misc.Launcher.
getBootstrapClassPath().getURLs();
// 遍历、输出根类加载器加载的全部URL
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
}
}
Extension Classloader
被称为扩展类加载器,它负责加载 JRE 的扩展目录(%JAVA_HOME%/jre/lib/ext
或者由java.ext.dirs
系统属性指定的目录)中 JAR 包的类。
通过这种方式,就可以为 Java 扩展核心类以外的新功能,只要我们把自己开发的类打包成 JAR 文件,然后放入JAVA_HOME/jre/lib/ext
路径即可。
System Classloader
被称为系统(也称为应用)类加载器,它负责在 JVM 启动时加载来自 java 命令的-classpath
选项、java.class.path
系统属性,或 CLASSPATH 环境变量所指定的 JAR 包和类路径。程序可以通过ClassLoader
的静态方法getSystemClassLoader()
来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以类加载器作为父加载器。
类加载机制
JVM 的类加载机制主要有如下 3 种。
- 全盘负责。所谓全盘负责,就是当一个类加载器负责加载某个 Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
- 父类委托。所谓父类委托,则是先让 parent(父)类加载器试图加载该 Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 缓存机制。缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区中。这就是为什么修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因。
提示
类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。
除了可以使用 Java 提供的类加载器之外,也可以实现自己的类加载器,自定义的类加载器通过继承ClassLoader
来实现。JVM 中这 4 种类加载器的层次结构如图所示。
public class ClassLoaderPropTest {
public static void main(String[] args)
throws IOException {
// 获取系统类加载器
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemLoader);
/*
获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定
如果操作系统没有指定CLASSPATH环境变量,则默认以当前路径作为
系统类加载器的加载路径
*/
Enumeration<URL> em1 = systemLoader.getResources("");
while (em1.hasMoreElements()) {
System.out.println(em1.nextElement());
}
// 获取系统类加载器的父类加载器,得到扩展类加载器
ClassLoader extensionLoader = systemLoader.getParent();
System.out.println("扩展类加载器:" + extensionLoader);
System.out.println("扩展类加载器的加载路径:"
+ System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parent: "
+ extensionLoader.getParent());
}
}
系统类加载器:
sun.misc.Launcher$AppClassLoader@18b4aac2
>file:/E:/Project/demo/out/production/test/
扩展类加载器:sun.misc.Launcher$ExtClassLoader@74a14482
扩展类加载器的加载路径:D:\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
扩展类加载器的 parent:null
表面上看扩展类加载器的父加载器是null
,并不是根类加载器。这是因为根类加载器并没有继承ClassLoader
抽象类,所以扩展类加载器的getParent()
方法返回null
。但实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是 Java 实现的。
系统类加载器是AppClassLoader
的实例,扩展类加载器是ExtClassLoader
的实例。
类加载器加载 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()
方法来实现自己的载入过程。
创建并使用自定义的类加载器
JVM 中除根类加载器之外的所有类加载器都是ClassLoader
子类的实例,可以通过扩展ClassLoader
的子类,并重写该ClassLoader
所包含的方法来实现自定义的类加载器。
ClassLoader
类有如下两个关键方法。
oadClass(String name, boolean resolve)
:该方法为ClassLoader
的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader
的该方法来获取指定类对应的Class
对象。findClass(String name)
:根据二进制名称来查找类。
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 的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。
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 源文件。
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* @author 空~ 2022/8/22 周一 17:05
**/
public class CompileClassLoader extends ClassLoader {
// 定义一个主方法
public static void main(String[] args) throws Exception {
// 如果运行该程序时没有参数,即没有目标类
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", (new String[0]).getClass());
Object[] argsArray = {progArgs};
main.invoke(null, argsArray);
}
// 读取一个文件的内容
private byte[] getBytes(String filename)
throws IOException {
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int) len];
try (
FileInputStream 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命令
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try {
// 其他线程都等待这个线程完成
p.waitFor();
} catch (InterruptedException ie) {
System.out.println(ie);
}
// 获取javac线程的退出值
int ret = p.exitValue();
// 返回编译是否成功
return ret == 0;
}
// 重写ClassLoader的findClass方法
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;
}
}
提供一个简单的主类,该主类无须编译就可以使用上面的CompileClassLoader
来运行它。
public class Hello {
public static void main(String[] args) {
for (String arg : args) {
System.out.println("运行Hello的参数:" + arg);
}
}
}
因为cmd
默认编码为GBK
,直接编译会导致乱码,先将需要的 Java 文件编码格式改为相应类型。
保证CompileClassLoader
类和Hello
在同一个包下(不带包最好),先将CompileClassLoader
类编译成 class 文件。
javac CompileClassLoader.java
然后就可以直接使用 java 命令编译 Hello 类了。
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
环境变量中。
public class URLClassLoaderTest {
private static Connection conn;
// 定义一个获取数据库连接的方法
public static Connection getConn(String url,
String user, String pass) throws Exception {
if (conn == null) {
// 创建一个URL数组
URL[] urls = {new URL(
"mysql-connector-java-5.1.47.jar")};
// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加载MySQL的JDBC驱动,并创建默认实例
Driver driver = (Driver) myClassLoader.
loadClass("com.mysql.jdbc.Driver").newInstance();
// 创建一个设置JDBC连接属性的Properties对象
Properties props = new Properties();
// 至少需要为该对象传入user和password两个属性
props.setProperty("user", user);
props.setProperty("password", pass);
// 调用Driver对象的connect方法来取得数据库连接
conn = driver.connect(url, props);
}
return conn;
}
public static void main(String[] args) throws Exception {
System.out.println(getConn("jdbc:mysql://localhost:3306/bjpowernode"
, "root", "root"));
}
}
当我们创建URLClassLoader
时传入了一个 URL 数组参数,该ClassLoader
就可以从这系列 URL 指定的资源中加载指定类,这里的URL
可以以file:
为前缀,表明从本地文件系统加载;可以以http:
为前缀,表明从互联网通过HTTP
访问来加载;也可以以ftp:
为前缀,表明从互联网通过FTP
访问来加载……功能非常强大。
反射
什么是反射?
反射就是 Reflection,Java 的反射是指程序在运行期可以拿到一个对象的所有信息。
Class 类
除了int
等基本类型外,Java 的其他类型全部都是class
(包括interface
)。例如:
String
Object
Runnable
Exception
- ...
class
(包括interface
)的本质是数据类型(Type
)。无继承关系的数据类型无法赋值:
Number n = new Double(123.456); // OK
String s = new Double(123.456); // compile error!
而 class
是由 JVM 在执行过程中动态加载的。JVM 在第一次读取到一种 class
类型时,将其加载进内存。
每加载一种 class
,JVM 就为其创建一个 Class
类型的实例,并关联起来。注意:这里的 Class
类型是一个名叫 Class
的 class
。它长这样:
public final class Class {
private Class() {}
}
以 String
类为例,当 JVM 加载 String
类时,它首先读取 String.class
文件到内存,然后,为 String
类创建一个 Class
实例并关联起来:
Class cls = new Class(String);
这个 Class
实例是 JVM 内部创建的,查看 JDK 源码,可以发现 Class
类的构造方法是 private
,只有 JVM 能创建 Class
实例,自己的 Java 程序是无法创建 Class
实例的。
所以,JVM 持有的每个 Class
实例都指向一个数据类型(class
或 interface
):
┌───────────────────────────┐
│ Class Instance │──────> String
├───────────────────────────┤
│name = "java.lang.String" │
└───────────────────────────┘
┌───────────────────────────┐
│ Class Instance │──────> Random
├───────────────────────────┤
│name = "java.util.Random" │
└───────────────────────────┘
┌───────────────────────────┐
│ Class Instance │──────> Runnable
├───────────────────────────┤
│name = "java.lang.Runnable"│
└───────────────────────────┘
一个 Class
实例包含了该 class
的所有完整信息:
┌───────────────────────────┐
│ Class Instance │──────> String
├───────────────────────────┤
│name = "java.lang.String" │
├───────────────────────────┤
│package = "java.lang" │
├───────────────────────────┤
│super = "java.lang.Object" │
├───────────────────────────┤
│interface = CharSequence...│
├───────────────────────────┤
│field = value[],hash,... │
├───────────────────────────┤
│method = indexOf()... │
└───────────────────────────┘
由于 JVM 为每个加载的 class
创建了对应的 Class
实例,并在实例中保存了该 class
的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个 Class
实例,就可以通过这个 Class
实例获取到该实例对应的 class
的所有信息。
这种通过 Class
实例获取 class
信息的方法称为反射(Reflection)。
有三个方法获可以取一个 class
的 Class
实例:
方法一:直接通过一个 class
的静态变量 class
获取:
Class cls = String.class;
方法二:如果有一个实例变量,可以通过该实例变量提供的 getClass()
方法获取:
String s = "Hello";
Class cls = s.getClass();
方法三:如果知道一个 class
的完整类名,可以通过静态方法 Class.forName()
获取:
Class cls = Class.forName("java.lang.String");
因为 Class
实例在 JVM 中是唯一的,所以,上述方法获取的 Class
实例是同一个实例。可以用 ==
比较两个 Class
实例:
Class cls1 = String.class;
String s = "Hello";
Class cls2 = s.getClass();
boolean sameClass = cls1 == cls2; // true
Class
实例比较和 instanceof
的差别:
Integer n = new Integer(123);
boolean b1 = n instanceof Integer; // true,因为n是Integer类型
boolean b2 = n instanceof Number; // true,因为n是Number类型的子类
boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
用 instanceof
不但匹配指定类型,还匹配指定类型的子类。而用 ==
判断 class
实例可以精确地判断数据类型,但不能作子类型比较。
通常情况下,应该用 instanceof
判断数据类型,因为面向抽象编程的时候,不关心具体的子类型。只有在需要精确判断一个类型是不是某个 class
的时候,才使用 ==
判断 class
实例。
因为反射的目的是为了获得某个实例的信息。因此,当拿到某个 Object
实例时,可以通过反射获取该 Object
的 class
信息:
void printObjectInfo(Object obj) {
Class cls = obj.getClass();
}
从 Class
实例获取获取的基本信息:
public class Main {
public static void main(String[] args) {
printClassInfo("".getClass());
printClassInfo(Runnable.class);
printClassInfo(java.time.Month.class);
printClassInfo(String[].class);
printClassInfo(int.class);
}
static void printClassInfo(Class cls) {
System.out.println("Class name: " + cls.getName());
System.out.println("Simple name: " + cls.getSimpleName());
if (cls.getPackage() != null) {
System.out.println("Package name: " + cls.getPackage().getName());
}
System.out.println("is interface: " + cls.isInterface());
System.out.println("is enum: " + cls.isEnum());
System.out.println("is array: " + cls.isArray());
System.out.println("is primitive: " + cls.isPrimitive());
}
}
数组(例如 String[]
)也是一种类,而且不同于 String.class
,它的类名是 [Ljava.lang.String;
。此外,JVM 为每一种基本类型如 int
也创建了 Class
实例,通过 int.class
访问。
如果获取到了一个 Class
实例,就可以通过该 Class
实例来创建对应类型的实例:
// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();
上述代码相当于 new String()
。通过 Class.newInstance()
可以创建类实例,它的局限是:只能调用 public
的无参数构造方法。带参数的构造方法,或者非 public
的构造方法都无法通过 Class.newInstance()
被调用。
动态加载
JVM 在执行 Java 程序的时候,并不是一次性把所有用到的 class 全部加载到内存,而是第一次需要用到 class 时才加载。例如:
public class Main {
public static void main(String[] args) {
if (args.length > 0) {
create(args[0]);
}
}
static void create(String name) {
Person p = new Person(name);
}
}
当执行 Main.java
时,由于用到了 Main
,因此,JVM 首先会把 Main.class
加载到内存。然而,并不会加载 Person.class
,除非程序执行到 create()
方法,JVM 发现需要加载 Person
类时,才会首次加载 Person.class
。如果没有执行 create()
方法,那么 Person.class
根本就不会被加载。
这就是 JVM 动态加载 class
的特性。
动态加载 class
的特性对于 Java 程序非常重要。利用 JVM 动态加载 class
的特性,才能在运行期根据条件加载不同的实现类。例如,Commons Logging 总是优先使用 Log4j,只有当 Log4j 不存在时,才使用 JDK 的 logging。利用 JVM 动态加载特性,大致的实现代码如下:
// Commons Logging优先使用Log4j:
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {
factory = createLog4j();
} else {
factory = createJdkLog();
}
boolean isClassPresent(String name) {
try {
Class.forName(name);
return true;
} catch (Exception e) {
return false;
}
}
这就是为什么只需要把 Log4j 的 jar 包放到 classpath
中,Commons Logging 就会自动使用 Log4j 的原因。
访问字段
对任意的一个 Object
实例,只要获取了它的 Class
,就可以获取它的一切信息。
如何通过 Class
实例获取字段信息。Class
类提供了以下几个方法来获取字段:
Field getField (name)
:根据字段名获取某个public
的field
(包括父类)Field getDeclaredField (name)
:根据字段名获取当前类的某个field
(不包括父类)Field [] getFields ()
:获取所有public
的field
(包括父类)Field [] getDeclaredFields ()
:获取当前类的所有field
(不包括父类)
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public字段"score":
System.out.println(stdClass.getField("score"));
// 获取继承的public字段"name":
System.out.println(stdClass.getField("name"));
// 获取private字段"grade":
System.out.println(stdClass.getDeclaredField("grade"));
}
}
class Student extends Person {
public int score;
private int grade;
}
class Person {
public String name;
}
上述代码首先获取 Student
的 Class
实例,然后,分别获取 public
字段、继承的 public
字段以及 private
字段,打印出的 Field
类似:
public int Student.score
public java.lang.String Person.name
private int Student.grade
一个 Field
对象包含了一个字段的所有信息:
getName()
:返回字段名称,例如,"name"
;getType()
:返回字段类型,也是一个Class
实例,例如,String.class
;getModifiers()
:返回字段的修饰符,它是一个int
,不同的 bit 表示不同的含义。
以 String
类的 value
字段为例,它的定义是:
public final class String {
private final byte[] value;
}
用反射获取该字段的信息,代码如下:
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
获取字段值
利用反射拿到字段的一个 Field
实例只是第一步,还可以拿到一个实例对应的该字段的值。
例如,对于一个 Person
实例,可以先拿到 name
字段对应的 Field
,再获取这个实例的 name
字段的值:
public class Main {
public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
上述代码先获取 Class
实例,再获取 Field
实例,然后,用 Field.get(Object)
获取指定实例的指定字段的值。
运行代码,如果不出意外,会得到一个 IllegalAccessException
,这是因为 name
被定义为一个 private
字段,正常情况下,Main
类无法访问 Person
类的 private
字段。要修复错误,可以将 private
改为 public
,或者,在调用 Object value = f.get(p);
前,先写一句:
f.setAccessible(true);
调用 Field.setAccessible(true)
的意思是,别管这个字段是不是 public
,一律允许访问。
加上上述语句,再运行代码,就可以打印出 private
字段的值。
提示
如果使用反射可以获取 private
字段的值,那么类的封装还有什么意义?
答案是正常情况下,我们总是通过 p.name
来访问 Person
的 name
字段,编译器会根据 public
、protected
和 private
决定是否允许访问字段,这样就达到了数据封装的目的。
而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
此外,setAccessible(true)
可能会失败。如果 JVM 运行期存在 SecurityManager
,那么它会根据规则进行检查,有可能阻止setAccessible(true)
。例如,某个 SecurityManager
可能不允许对 java
和 javax
开头的 package
的类调用setAccessible(true)
,这样可以保证 JVM 核心库的安全。
设置字段值
通过 Field 实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。
设置字段值是通过 Field.set(Object, Object)
实现的,其中第一个 Object
参数是指定的实例,第二个 Object
参数是待修改的值。示例代码如下:
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
打印的 name
字段从 Xiao Ming
变成了 Xiao Hong
,说明通过反射可以直接修改字段的值。
同样的,修改非 public
字段,需要首先调用 setAccessible(true)
。
调用方法
能通过 Class
实例获取所有 Field
对象,同样的,可以通过 Class
实例获取所有 Method
信息。
Class
类提供了以下几个方法来获取 Method
:
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public方法getScore,参数为String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的public方法getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取private方法getGrade,参数为int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
}
}
class Student extends Person {
public int getScore(String type) {
return 99;
}
private int getGrade(int year) {
return 1;
}
}
class Person {
public String getName() {
return "Person";
}
}
上述代码首先获取 Student
的 Class
实例,然后,分别获取 public
方法、继承的 public
方法以及 private
方法,打印出的 Method
类似:
public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)
一个 Method
对象包含一个方法的所有信息:
getName()
:返回方法名称,例如:"getScore"
;getReturnType()
:返回方法返回值类型,也是一个 Class 实例,例如:String.class
;getParameterTypes()
:返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class}
;getModifiers()
:返回方法的修饰符,它是一个int
,不同的 bit 表示不同的含义。
调用普通方法
当我们获取到一个 Method
对象时,就可以对它进行调用。我们以下面的代码为例:
String s = "Hello world";
String r = s.substring(6); // "world"
如果用反射来调用 substring
方法,需要以下代码:
public class Main {
public static void main(String[] args) throws Exception {
// String对象:
String s = "Hello world";
// 获取String substring(int)方法,参数为int:
Method m = String.class.getMethod("substring", int.class);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r);
}
}
注意到 substring()
有两个重载方法,需要获取的是 String substring(int)
这个方法。
对 Method
实例调用 invoke
就相当于调用该方法,invoke
的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
调用静态方法
如果获取到的 Method 表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以 invoke
方法传入的第一个参数永远为 null
。以 Integer.parseInt(String)
为例:
public class Main {
public static void main(String[] args) throws Exception {
// 获取Integer.parseInt(String)方法,参数为String:
Method m = Integer.class.getMethod("parseInt", String.class);
// 调用该静态方法并获取结果:
Integer n = (Integer) m.invoke(null, "12345");
// 打印调用结果:
System.out.println(n);
}
}
调用非 public 方法
和 Field
类似,对于非 public
方法,虽然可以通过 Class.getDeclaredMethod()
获取该方法实例,但直接对其调用将得到一个 IllegalAccessException
。为了调用非 public
方法,通过 Method.setAccessible(true)
允许其调用:
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person();
Method m = p.getClass().getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(p, "Bob");
System.out.println(p.name);
}
}
class Person {
String name;
private void setName(String name) {
this.name = name;
}
}
此外,setAccessible(true)
可能会失败。如果 JVM 运行期存在 SecurityManager
,那么它会根据规则进行检查,有可能阻止 setAccessible(true)
。例如,某个 SecurityManager
可能不允许对 java
和 javax
开头的 package
的类调用 setAccessible(true)
,这样可以保证 JVM 核心库的安全。
多态
考察这样一种情况:一个 Person
类定义了 hello()
方法,并且它的子类 Student
也覆写了 hello()
方法,那么,从 Person.class
获取的 Method
,作用于 Student
实例时,调用的方法到底是哪个?
public class Main {
public static void main(String[] args) throws Exception {
// 获取Person的hello方法:
Method h = Person.class.getMethod("hello");
// 对Student实例调用hello方法:
h.invoke(new Student());
}
}
class Person {
public void hello() {
System.out.println("Person:hello");
}
}
class Student extends Person {
public void hello() {
System.out.println("Student:hello");
}
}
打印出的是 Student:hello
,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。上述的反射代码:
Method m = Person.class.getMethod("hello");
m.invoke(new Student());
实际上相当于:
Person p = new Student();
p.hello();
调用构造方法
通常使用 new
操作符创建新的实例:
Person p = new Person();
如果通过反射来创建新的实例,可以调用 Class 提供的 newInstance()
方法:
Person p = Person.class.newInstance();
调用 Class.newInstance()
的局限是,它只能调用该类的 public
无参数构造方法。如果构造方法带有参数,或者不是 public
,就无法直接通过 Class.newInstance()
来调用。
为了调用任意的构造方法,Java 的反射 API 提供了 Constructor
对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor
对象和 Method
非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:
public class Main {
public static void main(String[] args) throws Exception {
// 获取构造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);
// 获取构造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
}
}
通过 Class 实例获取 Constructor 的方法如下:
getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
注意 Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非 public
的 Constructor
时,必须首先通过 setAccessible(true)
设置允许访问。setAccessible(true)
可能会失败。
获取继承关系
当获取到某个 Class
对象时,实际上就获取到了一个类的类型:
Class cls = String.class; // 获取到String的Class
还可以用实例的 getClass()
方法获取:
String s = "";
Class cls = s.getClass(); // s是String,因此获取到String的Class
最后一种获取 Class
的方法是通过 Class.forName("")
,传入 Class
的完整类名获取:
Class s = Class.forName("java.lang.String");
这三种方式获取的 Class
实例都是同一个实例,因为 JVM 对每个加载的 Class
只创建一个 Class
实例来表示它的类型。
获取父类的 Class
有了 Class
实例,还可以获取它的父类的 Class
:
public class Main {
public static void main(String[] args) throws Exception {
Class i = Integer.class;
Class n = i.getSuperclass();
System.out.println(n);
Class o = n.getSuperclass();
System.out.println(o);
System.out.println(o.getSuperclass());
}
}
运行上述代码,可以看到,Integer
的父类类型是 Number
,Number
的父类是 Object
,Object
的父类是 null
。除 Object
外,其他任何非 interface
的 Class
都必定存在一个父类类型。
获取 interface
由于一个类可能实现一个或多个接口,通过 Class
就可以查询到实现的接口类型。例如,查询 Integer
实现的接口:
public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
运行上述代码可知,Integer
实现的接口有:
java.lang.Comparable
java.lang.constant.Constable
java.lang.constant.ConstantDesc
要特别注意:getInterfaces()
只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型:
public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class.getSuperclass();
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
Integer
的父类是 Number
,Number
实现的接口是 java.io.Serializable
。
此外,对所有 interface
的 Class
调用 getSuperclass()
返回的是 null
,获取接口的父接口要用 getInterfaces()
:
// java.io.FilterInputStream,因为DataInputStream继承自FilterInputStream
System.out.println(java.io.DataInputStream.class.getSuperclass());
// null,对接口调用getSuperclass()总是返回null,获取接口的父接口要用getInterfaces()
System.out.println(java.io.Closeable.class.getSuperclass());
如果一个类没有实现任何 interface
,那么 getInterfaces()
返回空数组。
继承关系
当我们判断一个实例是否是某个类型时,正常情况下,使用 instanceof
操作符:
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true
如果是两个 Class
实例,要判断一个向上转型是否成立,可以调用 isAssignableFrom()
:
// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
动态代理
比较 Java 的 class
和 interface
的区别:
- 可以实例化
class
(非abstract
); - 不能实例化
interface
。
所有 interface
类型的变量总是通过某个实例向上转型并赋值给接口类型变量的:
CharSequence cs = new StringBuilder();
有没有可能不编写实现类,直接在运行期创建某个 interface
的实例呢?
这是可能的,因为 Java 标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个 interface
的实例。
什么叫运行期动态创建?听起来好像很复杂。所谓动态代理,是和静态相对应的。来看静态代码怎么写:
定义接口:
public interface Hello {
void morning(String name);
}
编写实现类:
public class HelloWorld implements Hello {
public void morning(String name) {
System.out.println("Good morning, " + name);
}
}
创建实例,转型为接口并调用:
Hello hello = new HelloWorld();
hello.morning("Bob");
这种方式就是通常编写代码的方式。
还有一种方式是动态代码,我们仍然先定义了接口 Hello
,但是我们并不去编写实现类,而是直接通过 JDK 提供的一个 Proxy.newProxyInstance()
创建了一个 Hello
接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,称为动态代码。JDK 提供的动态创建接口对象的方式,就叫动态代理。
一个最简单的动态代理实现如下:
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
在运行期动态创建一个 interface
实例的方法如下:
- 定义一个
InvocationHandler
实例,它负责实现接口的方法调用; - 通过
Proxy.newProxyInstance()
创建interface
实例,它需要 3 个参数:- 使用的
ClassLoader
,通常就是接口类的ClassLoader
; - 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的
InvocationHandler
实例。
- 使用的
- 将返回的
Object
强制转型为接口。
动态代理实际上是 JVM 在运行期动态创建 class 字节码并加载的过程,把上面的动态代理改写为静态实现类大概长这样:
public class HelloDynamicProxy implements Hello {
InvocationHandler handler;
public HelloDynamicProxy(InvocationHandler handler) {
this.handler = handler;
}
public void morning(String name) {
handler.invoke(
this,
Hello.class.getMethod("morning", String.class),
new Object[] { name });
}
}
其实就是 JVM 帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码)。
好玩的。
interface Student {
void eat();
void run();
void write();
}
public class DynamicProxy {
public static void main(String[] args) {
// 小韭菜学生类
Student ordinaryStudents = new OrdinaryStudents();
ordinaryStudents.eat();
ordinaryStudents.write();
/*
现在有一位特殊的学生,他是区长的儿子,我们自然要对他额外照顾,要给他加一下功能。
一种思路是定义一个类:区长的儿子类,他继承自学生类,但世上儿子千千万,有区长的儿子,也有市长的儿子,更有省长的儿子,不能把他们挨个定义出来,
现在就可以使用动态代理机制,动态的给区长的儿子加上功能,以后碰到市长、省长的儿子也同样处理。
InvocationHandler作用就是,当代理对象的原本方法被调用的时候,会重定向到一个方法,
这个方法就是InvocationHandler里面定义的内容,同时会替代原本方法的结果返回。
InvocationHandler接收三个参数:proxy,代理后的实例对象。 method,对象被调用方法。args,调用时的参数。
*/
InvocationHandler handler = (proxy, method, handlerArgs) -> {
// 从定义eat方法。
if ("eat".equals(method.getName())) {
System.out.println("我可以吃香喝辣!");
return null;
}
// 从定义write方法。
if ("write".equals(method.getName())) {
System.out.println("我的作文题目是《我的区长父亲》。");
// 调用普通学生类的write方法,流程还是要走的,还是要交一篇作文上去,不能太明目张胆。
method.invoke(ordinaryStudents, handlerArgs);
System.out.println("我的作文拿了区作文竞赛一等奖!so easy!");
return null;
}
return null;
};
/*
对这个实例对象代理生成一个代理对象。
被代理后生成的对象,是通过People接口的字节码增强方式创建的类而构造出来的。它是一个临时构造的实现类的对象。
loader和interfaces基本就是决定了这个类到底是个怎么样的类。而h是InvocationHandler,决定了这个代理类到底是多了什么功能.
通过这些接口和类加载器,拿到这个代理类class。然后通过反射的技术复制拿到代理类的构造函数,
最后通过这个构造函数new个一对象出来,同时用InvocationHandler绑定这个对象。
最终实现可以在运行的时候才切入改变类的方法,而不需要预先定义它。
*/
Student sonOfDistrict = (Student) Proxy.newProxyInstance(
ordinaryStudents.getClass().getClassLoader(), ordinaryStudents.getClass().getInterfaces(),
handler);
sonOfDistrict.eat();
sonOfDistrict.write();
}
}
/**
* 小韭菜,能跑,能吃,能写作文。
*/
class OrdinaryStudents implements Student {
@Override
public void eat() {
System.out.println("我在吃饭!");
}
@Override
public void run() {
System.out.println("我在跑步!");
}
@Override
public void write() {
System.out.println("我在写作文!");
}
}