Java的强大不仅体现在静态、面向对象范式上。Java还拥有强大的自省和运行时动态调用能力,我们称之为反射。没有反射,Java生态系统就像是无源之水。CRUD 小王子用不到,但想玩出花就要用到了。
灵魂三问:
- 对象是如何创造出来的?【有类说明书】
- 类说明书从哪来的?【Java 安装目录下的相关 jar 包、自己项目中 Java 源文件编译之后的 target 目录,甚至从网络中,从任何地方】
- 类说明书被谁加载进来的?【ClassLoader】
1. Java 的类与 Class
可以通过对象的 getClass()
可以拿到 Class 对象,其代表了运行时真实的类型信息。Java 中所有的对象都在堆(heap)中。
- RTTI(Run-Time Type Identification)运行时类型识别。
- 一个 Class 对象就是一个类的说明书,JVM 根据这个说明书创建出来一个类的实例。
- 静态变量本质是归属于 class 对象本身的(依附于说明书本身),不和任何实例绑定。
-
instatnceof
,既然存在运行时类型识别以及getClass()
方法,所以instanceof
就像是语法糖而已。 - 强制类型转化,运行时 JVM 知道对象实际是什么类型,所以在运行强制类型转化时如果很牵强,则会发生运行时异常
ClassCastException
。
2. 类加载与 ClassLoader
2.1 类加载
加载前是 .class
文件,加载后是 Class 对象。该对象表示了一个正在运行的 Java 程序中的 class 或者接口。
代表了每个类的说明书的 class 对象都存放在 JVM 的元空间(永久代)中。

Class 类对象(在 JVM 中代表了编译后的
.class
文件)在第一次使用时被加载。
例如继承体系是 Object <- Anical <- Cat <- WhiteCat。在第一次 new WhiteCat 时,实际上先要加载和调用相关父类。
把 IDEA 自动拼接后的命令复制出来,手动添加 -verbose:class
参数,顺便使用 grep 过滤一下,观察类的加载顺序:
$ "C:\Program Files\Java\jdk1.8.0_202\bin\java.exe" -verbose:class "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2.1\lib\idea_rt.jar=55950:C:\Program Files\JetBrains\IntelliJ IDEA 2019
.2.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_202\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib
\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jacces
s.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\nashorn.jar;C:\Program F
iles\Java\jdk1.8.0_202\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\j
dk1.8.0_202\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jce.
jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_202\
jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar;D:\Pro
jects\tmp\map-bean-converter\target\classes" com.github.hcsp.reflection.WhiteCat | grep hcsp
# 输出如下
[Loaded com.github.hcsp.reflection.Animal from file:/D:/Projects/tmp/map-bean-converter/target/classes/]
[Loaded com.github.hcsp.reflection.Cat from file:/D:/Projects/tmp/map-bean-converter/target/classes/]
[Loaded com.github.hcsp.reflection.WhiteCat from file:/D:/Projects/tmp/map-bean-converter/target/classes/]
# 不要过滤,输出全量,会发现 Object 类是最先加载的
[Opened C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
2.2 ClassLoader
Classloader负责从外部系统中加载一个类,而这个被加载的类:
- 这个
.class
文件对应的 Java 文件并不需要一定存在【真正需要的只是编译后的.class
文件】 - 这个
class
文件也不需要一定存在【文件本质是字节流,可以从网络中加载进来,查看 ClassLoader 中的defineClass
方法】 - 可以动态生成,在内存中凭空“捏造”【术语:动态字节码增强,这是丰富多彩的基石】
- 双亲委派加载模型
加载一个类时,调用 ClassLoader 类的实例方法 loadClass
,该方法按以下逻辑进行加载:
先看类是否已经加载过了,如尚未加载,则再看当前 ClassLoader 是否存在 parent
(parent class loader,用于委派。该字段由 JVM 提供),存在则调用 parent
的 loadClass
方法进行加载,不存在则尝试调用 JVM 内置的 ClassLoader。
最后该 Class 对象如果还是没加载到、也没找到,依然是 null,那么会调用 findClass
(在 ClassLoader 抽象类中的实现只是抛了个异常,因此需要继承该抽象类的时候进行完善,参见下方实战)。
出于安全性考虑,JDK 内置的类都是由最顶端的类加载器进行加载。
- Java 规范和 JVM 规范,二者之间是独立的,唯一的联系是字节码,这种分离提供了在 JVM 上可以运行其他语言的可能。
3. 反射与动态调用
反射,顾名思义,镜子里看到的东西。根据参数的不同,可以运行时动态决定响应的过程。反射是一个主动阅读 class 说明书的过程。
Class 对象上的 get*
方法可以获取当前及其父类的 public 方法或属性,想要获取所有非继承的属性或方法,可使用 getDeclaredMethod
或 getDeclaredField
等。
• Class
- Class.forName()
• Method
- Method.invoke
• Field
- Field.get
- 根据参数动态创建一个对象:
// 动态创建对象
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
String className = args[0];
Class c = Class.forName(className); // 传入全限定类名(FQCN),比如 java.lang.String
Object obj = c.getConstructor().newInstance();
}
}
- 根据参数动态调用一个方法:
package com.github.hcsp.string;
import java.lang.reflect.InvocationTargetException;
public class Cat {
public void catchMouse() {
System.out.println("catchMouse!");
}
public void beCute() {
System.out.println("beCute!");
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Cat cat = new Cat();
// 刻意这样换行写,方便打断点在调试器中观察
cat
.getClass()
.getMethod(args[0]) // 传入方法名,比如 beCute
.invoke(cat);
}
}
- 根据参数动态获取一个属性:
package com.github.hcsp.reflection;
public class Cat extends Animal {
public String[] leg;
public String head = "head";
public String tail = "tail";
public static int count;
public void catchMouse() {
System.out.println("catchMouse!");
}
public void beCute() {
System.out.println("beCute!");
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Cat cat = new Cat();
System.out.println(cat
.getClass()
.getField(args[0]) // 传入字段名,比如 head
.get(cat)
);
}
}
Tips:可以直接使用 IDEA 为 java 命令传参,或者自己手动拼接。

4. 实战
4.1 使用反射实现一个 Java Bean 到 Map 的转换器
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class MapBeanConverter {
// 传入一个遵守Java Bean约定的对象,读取它的所有属性,存储成为一个Map
// 例如,对于一个DemoJavaBean对象 { id = 1, name = "ABC" }
// 应当返回一个Map { id -> 1, name -> "ABC", longName -> false }
// 提示:
// 1. 读取传入参数bean的Class
// 2. 通过反射获得它包含的所有名为getXXX/isXXX,且无参数的方法(即getter方法)
// 3. 通过反射调用这些方法并将获得的值存储到Map中返回
public static Map<String, Object> beanToMap(Object bean) {
Map<String, Object> result = new HashMap<>();
Class klass = bean.getClass();
Arrays.stream(klass.getDeclaredMethods())
.filter(m -> {
StringBuilder name = new StringBuilder(m.getName()); // 使用 StringBuilder 防止待会CharAt时数组越界
// 过滤有参方法
if (m.getParameters().length != 0) {
return false;
}
// 过滤非 get 或 is 开头, 并且其后跟随非大写字母的方法
if (name.toString().startsWith("get") || name.toString().startsWith("is")) {
return Character.isUpperCase(name.charAt(name.toString().startsWith("get") ? 3 : 2));
} else {
return false;
}
})
.forEach(m -> {
String name = m.getName();
String roughName = name.substring(name.startsWith("get") ? 3 : 2);
try {
result.put(
Character.toLowerCase(roughName.charAt(0)) + roughName.substring(1),
m.invoke(bean)
);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
});
return result;
}
// 传入一个遵守Java Bean约定的Class和一个Map,生成一个该对象的实例
// 传入参数DemoJavaBean.class和Map { id -> 1, name -> "ABC"}
// 应当返回一个DemoJavaBean对象 { id = 1, name = "ABC" }
// 提示:
// 1. 遍历map中的所有键值对,寻找klass中名为setXXX,且参数为对应值类型的方法(即setter方法)
// 2. 使用反射创建klass对象的一个实例
// 3. 使用反射调用setter方法对该实例的字段进行设值
public static <T> T mapToBean(Class<T> klass, Map<String, Object> map) {
T result = null;
try {
result = klass.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
e.printStackTrace();
}
T finalResult = result;
map.forEach((name, value) -> {
String methodName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
try {
klass.getMethod(methodName, value.getClass()).invoke(finalResult, value);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
});
return result;
}
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
DemoJavaBean bean = new DemoJavaBean();
bean.setId(100);
bean.setName("AAAAAAAAAAAAAAAAAAA");
System.out.println(beanToMap(bean));
Map<String, Object> map = new HashMap<>();
map.put("id", 123);
map.put("name", "ABCDEFG");
System.out.println(mapToBean(DemoJavaBean.class, map));
}
public static class DemoJavaBean {
private Integer id;
private String name;
private String privateField = "privateField";
public int isolate() {
System.out.println(privateField);
return 0;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public String getName(int i) {
return name + i;
}
public void setName(String name) {
this.name = name;
}
public boolean isLongName() {
return name.length() > 10;
}
@Override
public String toString() {
return "DemoJavaBean{"
+ "id="
+ id
+ ", name='"
+ name
+ '\''
+ ", longName="
+ isLongName()
+ '}';
}
}
}
// 测试用例
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class MapBeanConverterTest {
@Test
public void test() {
MapBeanConverter.DemoJavaBean bean = new MapBeanConverter.DemoJavaBean();
bean.setId(100);
bean.setName("BBBBBBBBBBBBB");
Map<String, Object> resultMap = MapBeanConverter.beanToMap(bean);
Assertions.assertEquals(100, resultMap.get("id"));
Assertions.assertEquals("BBBBBBBBBBBBB", resultMap.get("name"));
Assertions.assertEquals(true, resultMap.get("longName"));
Assertions.assertNull(resultMap.get("olate"));
Map<String, Object> map = new HashMap<>();
map.put("id", 456);
map.put("name", "12345");
MapBeanConverter.DemoJavaBean resultBean =
MapBeanConverter.mapToBean(MapBeanConverter.DemoJavaBean.class, map);
Assertions.assertEquals(456, resultBean.getId());
Assertions.assertEquals("12345", resultBean.getName());
}
}
4.2 实现一个自定义的 ClassLoader
看一下 IDEA 拼接的命令行参数 -classpath
告诉了 JVM 去哪里寻找 .class
文件进行加载。而自定义的 ClassLoader 中可以实现运行时动态地加载指定位置(本地文件系统、甚至是网络中)的 .class
文件。
实现一个 ClassLoader,文档中鼓励覆盖 findClass
方法,而不是 loadClass
方法。
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
public class MyClassLoader extends ClassLoader {
// 存放字节码文件的目录
private final File bytecodeFileDirectory;
public MyClassLoader(File bytecodeFileDirectory) {
this.bytecodeFileDirectory = bytecodeFileDirectory;
}
// 还记得类加载器是做什么的么?
// "从外部系统中,加载一个类的定义(即Class对象)"
// 请实现一个自定义的类加载器,将当前目录中的字节码文件加载成为Class对象
// 提示,一般来说,要实现自定义的类加载器,你需要覆盖以下方法,完成:
//
// 1.如果类名对应的字节码文件存在,则将它读取成为字节数组
// 1.1 调用ClassLoader.defineClass()方法将字节数组转化为Class对象
// 2.如果类名对应的字节码文件不存在,则抛出ClassNotFoundException
//
// 一个用于测试的字节码文件可以在本项目的根目录找到
//
// 请思考:双亲委派加载模型在哪儿?为什么我们没有处理?
// 扩展阅读:ClassLoader类的Javadoc文档
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
if (b == null) {
throw new ClassNotFoundException(name);
} else {
return defineClass(name, b, 0, b.length);
}
}
/**
* 跟进文件名将二进制文件读取成字节流
*
* @param name .class 二进制文件的文件名
* @return 成功读取返回 byte[],未找到或者读取失败返回 null
*/
private byte[] loadClassData(String name) {
try {
return Files.readAllBytes(bytecodeFileDirectory.toPath().resolve(name + ".class"));
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) throws Exception {
File projectRoot = new File(System.getProperty("basedir", System.getProperty("user.dir")));
MyClassLoader myClassLoader = new MyClassLoader(projectRoot);
Class testClass = myClassLoader.loadClass("com.github.hcsp.MyTestClass");
Object testClassInstance = testClass.getConstructor().newInstance();
String message = (String) testClass.getMethod("sayHello").invoke(testClassInstance);
System.out.println(message);
}
}
根目录中提前准备一个 .class
文件:
public class MyTestClass {
public MyTestClass() {
}
public String sayHello() {
return "Hello";
}
}

5. 查看字节码的工具
- javap 工具
- IDEA 的 ASM ByteCode Viewer 插件
- Classpy 工具
网友评论