封面来源:碧蓝航线 深度回音 活动CG

本文涉及的代码:java8/lambda

参考链接:

0. 前言

初次见到 Mybatis-Plus 中的 LambdaQuery 时,其使用方法引用获取实体字段名称的方式为我对代码重构的理解提供了新思路,但那时并没有对其进行深究,仅仅是在使用 Mybatis-Plus 时更多地使用 LambdaQuery。

这次就来好好唠唠,如何通过 Java8 的方法引用来获取实体字段名称,以及背后的实现原理。

本文基于 JDK8 编写并在开篇直接给出通过方法引用获取字段名称的具体实现,而后通过对具体实现进行逐行分析完成全文编写。

1. 通过方法引用获取字段名称

1.1 具体实现

首先需要一个 可序列化 的函数式接口,由于 JDK 中并没有提供可序列化的函数式接口,因此需要自行定义一个:

1
2
3
4
5
6
/**
* @author mofan
* @date 2022/6/7 17:07
*/
public interface SFunction<T, R> extends Serializable, Function<T, R> {
}

然后可以通过方法引用获取到实体类的字段名称:

1
2
3
4
5
6
@Test
public void testReflectionUtil() {
SFunction<Person, String> function = Person::getName;
String fieldName = ReflectionUtil.getFieldName(function);
Assert.assertEquals("name", fieldName);
}

ReflectionUtil.getFieldName() 又是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @author mofan
* @date 2022/6/9 16:26
*/
public class ReflectionUtil {
public static final String GETTER_PREFIX = "get";
public static final String BOOLEAN_GETTER_PREFIX = "is";
public static final String LAMBDA_PREFIX = "lambda$";
private static final Map<SFunction<?, ?>, Field> CACHE = new ConcurrentHashMap<>();

public static <T, R> String getFieldName(SFunction<T, R> function) {
return getField(function).getName();
}

public static Field getField(SFunction<?, ?> function) {
return CACHE.computeIfAbsent(function, ReflectionUtil::findField);
}

private static Field findField(SFunction<?, ?> function) {
Field field;
String fieldName;
try {
Optional<SerializedLambda> serializedLambda = LambdaUtil.getSerializedLambda(function);
String implMethodName = serializedLambda.map(SerializedLambda::getImplMethodName).orElse("");
if (implMethodName.startsWith(GETTER_PREFIX) && implMethodName.length() > GETTER_PREFIX.length()) {
fieldName = Introspector.decapitalize(implMethodName.substring(GETTER_PREFIX.length()));
} else if (implMethodName.startsWith(BOOLEAN_GETTER_PREFIX) && implMethodName.length() > BOOLEAN_GETTER_PREFIX.length()) {
fieldName = Introspector.decapitalize(implMethodName.substring(BOOLEAN_GETTER_PREFIX.length()));
} else if (implMethodName.startsWith(LAMBDA_PREFIX)) {
throw new IllegalArgumentException("不支持 Lambda 表达式,请使用方法引用");
} else {
throw new IllegalArgumentException(implMethodName + "不是 Getter 方法引用");
}
String implClass = serializedLambda.map(SerializedLambda::getImplClass).orElse("").replace("/", ".");
Class<?> aClass = Class.forName(implClass, false, ClassUtils.getDefaultClassLoader());

// 通过 Spring 的 ReflectionUtils 获取 Field 对象
field = ReflectionUtils.findField(aClass, fieldName);
if (field != null) {
return field;
} else {
throw new NoSuchFieldException(fieldName);
}
} catch (Exception e) {
throw new IllegalArgumentException("字段信息获取失败 " + e.getMessage());
}
}
}

findField() 方法开始,首先使用了 LambdaUtil.getSerializedLambda() 获取被 Optional 包装的 SerializedLambda 对象,那 getSerializedLambda() 又是怎么实现的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LambdaUtil {

public static String getImplClass(Serializable serializable) throws Exception {
return getSerializedLambda(serializable)
.map(i -> i.getImplClass().replace("/", "."))
.orElse("");
}

public static Optional<SerializedLambda> getSerializedLambda(Serializable serializable) throws Exception {
Class<? extends Serializable> aClass = serializable.getClass();
if (aClass.isSynthetic()) {
Method method = aClass.getDeclaredMethod("writeReplace");
method.setAccessible(true);
return Optional.of((SerializedLambda) method.invoke(serializable));
}
return Optional.empty();
}
}

ReflectionUtil 中有三个方法:

1、String getFieldName(SFunction<T, R> function):获取字段名称对应的字符串;

2、Field getField(SFunction<?, ?> function):获取字段名称对应的 Field 对象。如果缓存中存在则直接从缓存中获取,否则通过反射获取并将其放入缓存中;

3、Field findField(SFunction<?, ?> function):通过反射获取字段名称对应的 Field 对象。

1.2 问题的抛出

不难看出上述具体实现中的要点就是通过 SerializedLambda 对象获取到使用的方法引用中的信息,而获取 SerializedLambda 对象则是通过反射调用 writeReplace() 方法,那么:

  • SerializedLambda 是什么?

  • writeReplace() 方法又是什么?

要回答这两个问题,得先弄清楚 Java 的序列化。

2. Java 的序列化

2.1 什么是序列化

序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。比如将一个对象序列化后可以将它的信息存储到磁盘中的某个文件里,也可以通过反序列化将文件里的信息读取出来并恢复成原来的对象。使用序列化可以让对象脱离程序独立存在。

如果想让某个类能被序列化,那么它 必须 实现 Serializable 接口。Serializable 接口只是一个 标记 接口,它里面没有任何抽象方法,它仅表示实现它的子类是可序列化的。

所有需要在网络上传输的类都应该是可序列化的,所有需要被保存在磁盘上的类也应该是可序列化的。

2.2 Java 序列化的细节

1、一个对象被序列化时,这个对象的类名、未被 transient 修饰的成员变量都会被序列化,对象的方法、类变量(被 static 修饰的变量)、被 transient 修饰的成员变量不会被序列化;

2、要一个成员变量不被序列化,可以用 transient 关键词修饰它;

3、如果类里面有引用类型的成员变量,那么这个引用类型也 必须 是可序列化的(或者使用 transient 修饰),否则这个类不能被序列化;

4、反序列化对象时,必须 有目标对象的 Class 文件;

5、通过网络、文件来传输序列化的对象时,必须 按照实际写入顺序读取。

2.3 序列化的简单示例

首先定义一个简单的 People 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author mofan
* @date 2022/6/22 19:46
*/
@Getter
@Setter
@AllArgsConstructor
public class People implements Serializable {
private static final long serialVersionUID = 8209986739933993768L;

public People() {
System.out.println("People的无参构造器");
}

private String name;
private int age;


@Override
protected Object clone() throws CloneNotSupportedException {
System.out.println("People的clone方法");
return super.clone();
}
}

然后使用下面的代码创建一个 People 对象并将其保存到 mofan.out 文件中:

1
2
3
4
5
6
7
8
9
@Test
public void testSerial() throws Exception {
People people = new People("Mofan", 20);
FileOutputStream fos = new FileOutputStream("mofan.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(people);
// 判断序列化生成的文件是否存在
Assert.assertTrue(new File("mofan.out").exists());
}

最后再使用下面的代码从 mofan.out 文件中反序列化生成 People 对象:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testDeSerial() throws Exception {
FileInputStream fis = new FileInputStream("mofan.out");
ObjectInputStream ois = new ObjectInputStream(fis);
/*
* 构造 People 对象时,并没有调用它的无参构造方法也没有调用 clone 方法
*/
People people = (People) ois.readObject();
Assert.assertEquals("Mofan", people.getName());
Assert.assertEquals(20, people.getAge());
}

运行上述反序列化的代码后观察 IDEA 控制台的输出会发现并没有输出任何内容,这说明反序列化创建对象时并没有调用目标对象的无参构造方法,也没有调用 clone() 方法,可以认为反序列化创建对象是一种特殊的创建对象的方式。

更多关于如何创建对象的内容可以参考本站《单身狗的福音——如何获得一个对象》一文(好中二的标题 😂)。

如果被序列化对象没有实现 Serializable 接口,那么就会抛出 NotSerializableException 异常。比如定义一个“简单”类:

1
2
3
4
5
6
7
8
9
10
/**
* @author mofan
* @date 2022/6/28 19:33
*/
@Getter
@Setter
@AllArgsConstructor
public class Simple {
private String stringField;
}

然后实例化“简单”对象,并对这个对象进行序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testNotImplSerializable() throws Exception {
Simple simple = new Simple("simple");

// 序列化时序列化对象未实现序列化接口
FileOutputStream fos = new FileOutputStream("simple.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
try {
oos.writeObject(simple);
Assert.fail();
} catch (IOException e) {
Assert.assertTrue(e instanceof NotSerializableException);
}
}

简单查看源码,在 ObjectOutputStreamwriteObject0() 方法中可以看到:

writeObject0方法部分源码

如果一个对象不是字符串、数组、枚举,同时也没有实现 Serializable 接口, 那么就会抛出 NotSerializableException 异常,并且这段代码也证明了 Serializable 接口仅仅是做一个标识,怎么进行序列化并不是由它完成的。

2.4 引用对象的序列化

在上述简单的示例中,我们定义了 People 类,它含有两个成员变量,类型分别是基本类型 int 和引用类型 String。 观察 String 源码可知,String 也实现了序列化接口。

在【2.2 序列化的细节】中说过:如果类里面有引用类型的成员变量,那么这个引用类型也 必须 是可序列化的(或者使用 transient 修饰),否则这个类不能被序列化。

序列化含有引用类型字段的对象

例如定义 Company 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author mofan
* @date 2022/6/23 15:24
*/
@Getter
@Setter
public class Company implements Serializable {
private static final long serialVersionUID = 2033276417972897957L;

private String companyName;
private List<People> employees;

public Company(String companyName, List<People> employees) {
this.companyName = companyName;
this.employees = employees;
}
}

Company 类中,有两个引用类型的成员变量,并且这两个引用类型对应的类都是可序列化的。

编写简单的测试方法对 Company 对象进行序列化、反序列化测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
@SuppressWarnings("unchecked")
public void testSerialReferenceType() throws Exception {
List<People> list = new ArrayList<>();
list.add(new People("mofan", 20));
list.add(new People("MOFAN", 21));
list.add(new People("默烦", 21));

Company hugeCompany = new Company("大公司", list);
Company smallCompany = new Company("小公司", list);

// 序列化对象
FileOutputStream fos = new FileOutputStream("company.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(list);
oos.writeObject(hugeCompany);
oos.writeObject(smallCompany);

// 依次反序列化对象
FileInputStream fis = new FileInputStream("company.out");
ObjectInputStream ois = new ObjectInputStream(fis);
List<People> deSerialList = (List<People>) ois.readObject();
Company deSerialHugeCompany = (Company) ois.readObject();
Company deSerialSmallCompany = (Company) ois.readObject();

Assert.assertSame(deSerialList, deSerialHugeCompany.getEmployees());
Assert.assertSame(deSerialList, deSerialSmallCompany.getEmployees());
Assert.assertNotSame(deSerialHugeCompany, deSerialSmallCompany);

Assert.assertNotSame(list, deSerialList);
}

运行测试方法后,序列化和反序列化都能够成功,并且还发现:序列化前的对象引用关系和反序列化后的对象引用关系是一样的。

比如,序列化前构造的 List 对象、hugeCompany 对象中引用的 List 对象和 smallCompany 对象中引用的 List 对象是同一个对象,而在反序列化后构造的 List 对象、deSerialHugeCompany 对象中引用的 List 对象和 deSerialSmallCompany 对象中引用的 List 对象也是同一个对象。当然了,序列化前的 List 对象和反序列化后 List 对象 肯定不是 同一个对象。

Java 序列化机制细节

1、所有保存到磁盘中的对象都有一个序列化编号;

2、当程序试图序列化一个对象时,程序将检查该对象是否被序列化过,只有该对象从未被序列化(在本次虚拟机中)过,系统才会将该对象转化为字节序列输出;

3、如果某个对象已经被序列化过了,程序只是输出一个序列化编号,而不是重新序列化该对象。

序列化含有未实现 Serializable 接口的引用类型字段的对象

定义一个 Complex 类,在这个类中引用未实现 Serializable 接口的 Simple 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author mofan
* @date 2022/6/28 19:33
*/
@Getter
@Setter
@AllArgsConstructor
public class Complex implements Serializable {

private static final long serialVersionUID = 7663091376889314225L;

private int intField;
private Simple simple;
}

最后编写测试方法测试 Complex 对象能否被序列化成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testNotImplSerializableFiled() throws Exception {
Simple simple = new Simple("test");
Complex complex = new Complex(100, simple);

// 序列化
FileOutputStream fos = new FileOutputStream("complex.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
try {
oos.writeObject(complex);
Assert.fail();
} catch (IOException e) {
// 抛出 NotSerializableException
Assert.assertTrue(e instanceof NotSerializableException);
}
}

在执行 oos.writeObject(complex); 时抛出了异常,将其捕获后进行断言,发现抛出了 NotSerializableException 异常,也就是说在序列化含有未实现 Serializable 接口的引用类型字段的对象时将抛出 NotSerializableException 异常。

使用 transient 修饰不需要被序列化的字段

当对象中的某些字段不需要被序列化时,可以使用 transient 关键词对这个字段进行修饰。比如可以使用 transient 修饰未实现 Serializable 接口的 Simple 类型的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author mofan
* @date 2022/6/28 19:40
*/
@Getter
@Setter
@AllArgsConstructor
public class TransientComplex implements Serializable {
private static final long serialVersionUID = -6616158309969761040L;

private String string;
private transient Simple simple;
private boolean bool;
}

这样的话即使 Simple 未实现序列化接口,在序列化 TransientComplex 对象时也不会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testTransient() throws Exception {
Simple simple = new Simple("TestString");
TransientComplex transientComplex = new TransientComplex("string", simple, true);

// 序列化
FileOutputStream fos = new FileOutputStream("transient.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(transientComplex);

// 反序列化
FileInputStream fis = new FileInputStream("transient.out");
ObjectInputStream ois = new ObjectInputStream(fis);
TransientComplex object = (TransientComplex) ois.readObject();

Assert.assertEquals("string", object.getString());
Assert.assertTrue(object.isBool());
// 虽然 Simple 没有实现序列化接口,但它被 transient 修饰,不会被序列化,因此也不会报错
Assert.assertNull(object.getSimple());
}

除了使用 transient 关键词外,也可以使用 static 关键词来使某个字段不被序列化,这很好理解,序列化是为了保存对象的状态,使用 static 修饰字段后,这个字段就变成了类的状态,因此序列化时也就会把它忽略了。只不过并不建议为了使某个字段不被序列化就 强行static 修饰它。

2.5 序列化的约束

序列化是将 Java 对象转换为字节序列,而反序列化就是这个过程的逆转,而在序列化或反序列化的过程中,他人对中间的字节流进行了篡改,那么反序列化出的对象将会存在问题。

如果在反序列化时能够对构造出的对象进行校验,那就可以在 一定程度上 防止他人篡改数据。在前面的示例中进行反序列化构造对象时都调用了 ObjectInputStreamreadObject() 方法,而我们也能自定义 readObject() 方法从而约束反序列化构造出的对象。

比如需要对 Student 对象序列化或反序列化,其中的 score 字段值的范围在 0 到 150 之间,为了防止他人将其篡改为非法数据,可以自定义 readObject() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author mofan
* @date 2022/7/1 15:51
*/
@Getter
@Setter
@AllArgsConstructor
public class Student implements Serializable {
private static final long serialVersionUID = -4539491935997507115L;

private String name;
private int score;

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// 调用默认的反序列化方法
ois.defaultReadObject();

// 逻辑判断
if (score < 0 || score > 150) {
throw new IllegalArgumentException("学生分数异常");
}
}
}

然后使用一个 score 字段值为 -1Student 对象进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testReadObject() throws Exception {
Student student = new Student("mofan", -1);

// 序列化
FileOutputStream fos = new FileOutputStream("student.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(student);

// 反序列化
FileInputStream fis = new FileInputStream("student.out");
ObjectInputStream ois = new ObjectInputStream(fis);
try {
ois.readObject();
Assert.fail();
} catch (Exception e) {
Assert.assertTrue(e instanceof IllegalArgumentException);
}
}

重写的 readObject() 方法会在反序列化时调用,因此也会在反序列化时抛出 IllegalArgumentException 异常。那问题又来了,为什么会调用自定义的 readObject() 方法呢?

这一切都在 ObjectStreamClass 类(ObjectStreamClass 是序列化和反序列化实现的类描述符)的私有构造方法中:

ObjectStreamClass的私有构造方法

不难看出是利用了反射来调用了自定义的 readObject() 方法。

有两个细节:

1、 如果类是 Externalizable 的子类,那么自定义的 readObject() 方法不会被反射调用。 Externalizable 接口也是一个序列化接口,这后面再说。

2、就算字段被 transient 关键词修饰,只要在自定义的 writeObject()readObject() 方法里显式声明了需要序列化和反序列化被修饰的字段,那么这个字段也会被序列化和反序列化。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author mofan
* @date 2022/7/2 21:27
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = -4647801022792976129L;

private String userName;
private transient String password;

private void writeObject(ObjectOutputStream oos) throws Exception {
oos.defaultWriteObject();

oos.writeObject(this.password);
}

private void readObject(ObjectInputStream ois) throws Exception{
ois.defaultReadObject();

this.password = (String) ois.readObject();
}
}

ObjectStreamClass 类的私有构造方法中还提到了 readObjectNoData()writeReplace()readResolve() 方法,那这些方法又有什么用呢?

2.6 readObjectNoData 方法

readObjectNoData() 方法用于正确初始化反序列化对象。比如现有 Vip 类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author mofan
* @date 2022/7/2 21:38
*/
@Getter
@Setter
public class Vip implements Serializable {

private static final long serialVersionUID = 5125855385522887545L;

private String level;
}

然后构建一个 Vip 对象并将其序列化存储到磁盘上:

1
2
3
4
5
6
7
8
9
10
@Test
public void test() throws Exception {
// Vip 还未继承 User
Vip vip = new Vip();
vip.setLevel("2");

FileOutputStream fos = new FileOutputStream("vip.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(vip);
}

假设此时需要 Vip 继承 User,这时对 vip.out 反序列化并构造 Vip 对象,显然构造出的对象中的 userNamepasswordnull

1
2
3
4
5
6
7
8
9
@Test
public void testReadObjectNoData() throws Exception {
FileInputStream fis = new FileInputStream("vip.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Vip newVip = (Vip) ois.readObject();

Assert.assertNull(newVip.getUserName());
Assert.assertNull(newVip.getPassword());
}

在实际需求下 null 值数据是非法的,需要为它们设置默认值,这时就可以在 User 类中增加自定义的 readObjectNoData() 方法:

1
2
3
4
5
6
7
8
public class User implements Serializable {
// 省略其他内容

private void readObjectNoData() {
this.userName = "Unknown";
this.password = "***";
}
}

这时再反序列化构造对象就会使未被序列化的字段设置上默认值:

1
2
3
4
5
6
7
8
9
@Test
public void testReadObjectNoData() throws Exception {
FileInputStream fis = new FileInputStream("vip.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Vip newVip = (Vip) ois.readObject();

Assert.assertEquals("Unknown", newVip.getUserName());
Assert.assertEquals("***", newVip.getPassword());
}

当字节信息接收方拓展了发送方发送的字节信息指向的类时,或者字节信息被篡改时,自定义 readObjectNoData() 方法可以保证数据的基本正确性。

2.7 隐藏方法 - writeReplace

实现了 Serializable 接口的类中定义 writeReplace() 方法,其方法签名如下:

1
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException

这是一个隐藏方法,当 JVM 对对象进行序列化时,如果对象中含有此方法,那么 writeReplace() 方法会在 writeObject() 之后 调用。也就是说,一旦定义了 writeReplace() 方法,那么由 writeObject() 序列化的对象会被完全丢弃,被序列化后存储到磁盘的对象是 writeReplace() 所返回的。

除此之外,writeReplace() 方法 所返回的对象也必须是可序列化的, 否则会抛出 NotSerializableException 异常。

定义一个实现了 Serializable 接口的类,并且这个类中含有 writeReplace() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author mofan
* @date 2022/6/28 19:58
*/
@Getter
@Setter
@AllArgsConstructor
public class City implements Serializable {
private static final long serialVersionUID = 2376268232459336285L;

private String name;
private int area;

Object writeReplace() throws ObjectStreamException {
// 返回的对象必须是可序列化的
return new People("mofan", 20);
}
}

编写测试方法对 City 对象进行序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testWriteReplace() throws Exception {
City city = new City("Chengdu", 143);

// 序列化
FileOutputStream fos = new FileOutputStream("chengdu.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(city);

// 反序列化
FileInputStream fis = new FileInputStream("chengdu.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Object object = ois.readObject();

Assert.assertTrue(object instanceof People);
People people = (People) object;
Assert.assertEquals("mofan", people.getName());
Assert.assertEquals(20, people.getAge());
}

还可以查看序列化生成的 chengdu.out 文件内容:

chengdu.out

虽然这个文件的内容我们难以读懂,但是能看到 indi.mofan.serial.Peoplemofan 等字样,而前者就是 People 类的全限定名称,这也表示被序列化存储到磁盘上的对象信息是 writeReplace() 方法的返回值。

2.8 隐藏方法 - readResolve

writeReplace() 相对的就是 readResolve() 方法,readResolve() 方法会在 readObject() 方法 之后 调用。也就是说,一旦定义了 readResolve() 方法,那么由 readObject() 方法反序列化所构造的对象会被完全丢弃,取而代之的是由 readResolve() 方法所返回的对象。

writeReplace() 方法不同的是,readResolve() 方法返回的对象 不一定 得是可序列化的。

定义一个实现了 Serializable 接口的类,并且这个类中含有 readResolve() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author mofan
* @date 2022/6/28 20:51
*/
@Getter
@Setter
@AllArgsConstructor
public class Fruit implements Serializable {
private static final long serialVersionUID = 4088817990272523988L;

private String name;
private String color;
private double size;

Object readResolve() throws ObjectStreamException {
// 返回的对象不一定是可序列化的
return new Simple("Simple");
}
}

编写测试方法对 Fruit 对象进行序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testReadResolve() throws Exception {
Fruit fruit = new Fruit("orange", "orange", 2.12);

// 序列化
FileOutputStream fos = new FileOutputStream("orange.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(fruit);

// 反序列化
FileInputStream fis = new FileInputStream("orange.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Object object = ois.readObject();

Assert.assertTrue(object instanceof Simple);
Simple simple = (Simple) object;
Assert.assertEquals("Simple", simple.getStringField());
}

同样也查看序列化生成的 orange.out 文件内容:

orange.out

在这个文件中可以看到 indi.mofan.serial.Fruit 的字样,这也是 Fruit 类的全限定名称。根据前面的经验可以知道,被序列化存储到磁盘上的对象信息依旧是 Fruit 对象的信息,只不过由于存在的 readResolve() 方法使得反序列化返回的对象是 readResolve() 方法返回的对象。

单例模式的增强

反序列化构造对象是一种构造对象的特殊方式,那么这就有可能带来一个问题: 可序列化的单例对象可能并不单例。

采用静态嵌套类方式实现单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author mofan
* @date 2022/7/1 12:53
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 5603073114730981002L;

private Singleton() {
}

private static class SingletonHolder {
private static final Singleton SINGLETON = new Singleton();
}

public static synchronized Singleton getSingleton() {
return SingletonHolder.SINGLETON;
}
}

Singleton 实现了 Serializable 接口,尝试序列化这个单例对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testSingleton() throws Exception {
// 序列化
FileOutputStream fos = new FileOutputStream("singleton.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(Singleton.getSingleton());

// 反序列化
FileInputStream fis = new FileInputStream("singleton.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton singleton = (Singleton) ois.readObject();

Assert.assertNotSame(Singleton.getSingleton(), singleton);
}

上方测试方法能够成功通过,通过最后的断言可知,反序列化构造的对象与序列化前的单例对象不是同一个对象。这时就可以利用 readResolve() 方法来解决这个问题,在 Singleton 类中定义 readResolve() 方法,然后返回单例对象即可。

1
2
3
4
5
6
7
8
public class Singleton implements Serializable {

// 省略其他实现

private Object readResolve() throws ObjectStreamException {
return getSingleton();
}
}

2.9 Externalizable 接口

除了实现 Serializable 接口可以让对象具备可序列化的特性外,实现 Externalizable 接口也可以达到同样的效果,只不过这个接口不再是一个标记接口,需要重写它里面的 writeExternal()readExternal() 抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* @author mofan
* @date 2022/6/28 20:59
*/
@Getter
@Setter
public class Animal implements Externalizable {
private static final long serialVersionUID = -3303909578015151048L;

private String type;
private int age;

public Animal() {
// Externalizable 反序列化时会调用无参构造方法
System.out.println("Animal 的无参构造器");
}

public Animal(String type, int age) {
this.type = type;
this.age = age;
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 序列化时使用
out.writeObject(this.type);
out.writeInt(this.age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 反序列化使用。设置字段值的顺序要和序列化时一致。
this.type = (String) in.readObject();
this.age = in.readInt();
}
}

编写测试方法对 Animal 对象进行序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testExternalizable() throws Exception {
Animal animal = new Animal("cat", 1);

// 序列化
FileOutputStream fos = new FileOutputStream("cat.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(animal);

// 反序列化
FileInputStream fis = new FileInputStream("cat.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Object object = ois.readObject();

Animal cat = (Animal) object;
Assert.assertEquals("cat", cat.getType());
Assert.assertEquals(1, cat.getAge());
}

运行测试方法后,断言能完全通过,并且在控制台打印出:

Animal 的无参构造器

通过控制台打印出来的内容可知, 实现 Externalizable 接口来反序列化时会调用目标对象的无参构造方法, 然后在 readExternal() 方法中对成员变量赋值,而实现 Serializable 接口进行反序列化时是不会调用任何构造器的。

Externalizable 接口中有两个抽象方法,其中序列化时使用 writeExternal(),反序列化时使用 readExternal()

注意:

1、相比于实现 Serializable 接口,实现 Externalizable 接口时,无参构造方法是 必要 的。

2、在 readExternal() 方法中设置字段值的顺序要和 writeExternal() 方法中读取字段值的顺序一致。

3、就算字段被 transient 关键词修饰,只要在重写的方法里显式声明了需要序列化和反序列化被修饰的字段,那么这个字段也会被序列化和反序列化。

2.10 序列化的版本

在实现了序列化接口的类中经常能看到以下一行代码:

1
private static final long serialVersionUID = 5125855385522887545L;

反序列化 Java 对象必须要有该对象的 Class 文件,但随着项目的迭代升级,系统的 Class 文件也在迭代升级,为了保证两个 Class 文件的兼容性,Java 的序列化机制为序列化类 默认 提供了一个名为 serialVersionUID 的唯一标识符(可以认为是 Java 类的序列化版本)。如果没有显式声明 serialVersionUID 的值,当类发生迭代升级后,默认提供的 serialVersionUID 值也会发生变化。

当一个类迭代升级后,只要它的 serialVersionUID 值不变,序列化机制就会把它们当成是同一个序列化版本,也就可以反序列化成功,否则抛出 InvalidClassException 异常终止发序列化过程。

因此建议凡是实现了 Serializable 接口的类,都应当显式声明 serialVersionUID 的值。

2.11 serialPersistentFields

在 JDK 14 中引入了 @Serial 注解,该注解只用于标记,和 @Override@FunctionalInterface 等注解类似,@Serial 注解用于标记与序列化相关的方法和字段。

与序列化相关的字段除了 serialVersionUID 外,还有 serialPersistentFields。比如在 String 类的源码中可以看到:

1
2
@java.io.Serial
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

一个实现了 Serialzable 接口的类,默认情况下所有非 transient、非 static 的字段都会被序列化,但如果定义了 serialPersistentFields 字段,那么只有 serialPersistentFields 里添加的字段才会被序列化。如果一个字段被 transient 修饰,但又被添加到 serialPersistentFields 字段中,这个字段依旧会被序列化。serialPersistentFields 的优先级比 transient 高,而被 static 修饰的字段始终不能被序列化。

使用细节

  • serialPersistentFields 字段必须是 ObjectStreamField[] 类型,且被 private static final 修饰,否则不生效。
  • 如果 serialPersistentFields 字段值为 null,也不会生效;但如果字段值是空数组,比如 {}new ObjectStreamField[0],则不会序列化类中的任何字段。
  • 如果声明了类中并不存在的字段,在序列化时抛出 InvalidClassException 异常。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author mofan
* @date 2023/6/15 21:34
*/
@Getter
@Setter
public class Computer implements Serializable {

@Serial
private static final long serialVersionUID = 455386781881536390L;
@Serial
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("brand", String.class),
// 可以序列化被 transient 字段,但不能序列化 static 字段
new ObjectStreamField("price", BigDecimal.class)
};

private String brand;
private String size;
private transient BigDecimal price;

public static String staticStr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Test
@SneakyThrows
public void testSerializedComputer() {
Computer computer = buildComputer();

String filePath = "./target/rog.out";
FileOutputStream fos = new FileOutputStream(filePath);
ObjectOutputStream oos = new ObjectOutputStream(fos);
try (fos; oos) {
oos.writeObject(computer);
} catch (IOException e) {
Assertions.fail();
}
assertThat(new File(filePath))
.isNotEmpty()
.content()
.doesNotContain("Republic of Gamers");

FileInputStream fis = new FileInputStream(filePath);
ObjectInputStream ois = new ObjectInputStream(fis);
try (fis; ois) {
Computer result = (Computer) ois.readObject();
assertThat(result).extracting(Computer::getBrand, Computer::getSize, Computer::getPrice, i -> Computer.staticStr)
.containsSequence("ROG", null, new BigDecimal("10000.00"), "Republic of Gamers");
} catch (IOException e) {
Assertions.fail();
}
}

private Computer buildComputer() {
Computer computer = new Computer();
computer.setBrand("ROG");
computer.setPrice(new BigDecimal("10000.00"));
computer.setSize("16 inch");
Computer.staticStr = "Republic of Gamers";
return computer;
}

序列化的内容就说这么多,也知道了 writeReplace() 方法是干嘛的,但现在又出现了一个问题:虽然定义的函数式接口 SFunction 继承了 Serializable 接口,但并没有像前文讲的那样显式声明 writeReplace() 方法,并且还可以反射调用 writeReplace() 方法获取到 SerializedLambda 对象。这又是为什么呢?

这就不得不说说 Lambda 表达式的序列化了。

3. Lambda 表达式的序列化

3.1 LambdaMetafactory

Lambda 表达式或方法引用使用到的自定义的函数式接口 SFunction 以及 Java 中自带的函数式接口虽然只是一个接口,但最终在程序中必定也是一个实现类的实例对象,不然程序怎么能够正常执行呢?这些实例对象的创建肯定是在程序运行时动态创建的,毕竟使用者并没有显式创建它们。

说到这些实例对象的创建就不得不谈到 LambdaMetafactory 类了,光从类名就不难看出这是一个工厂类,而且是和 Lambda 表达式相关的工厂类,那这个工厂类是用来创建什么对象呢?

很显然是用来创建 Lambda 表达式或方法引用的实现类的实例对象。证据可以查看 LambdaMetafactory 的首段注释:

LambdaMetafactory的首段注释

LambdaMetafactory 的顶部有大量的注释,不妨就从这些注释出发,探索 Lambda 表达式序列化的奥秘。

LambdaMetafactory 中有且仅有两个静态方法,分别是 metafactory()altMetafactory()。按照官方给出的注释信息,metafactory() 是构造实例对象的标准版,altMetafactory() 则是备用版,是标准版的概括。备用版对生成的函数对象提供了额外的控制,并且增加了管理函数对象的某些特性的能力,其中有个特性便是 可序列化性

有一段的注释内容如下:

函数对象的可序列化性

这段文字给出两个信息:

1、生成的函数对象 默认 不具备可序列化性,除非被 FLAG_SERIALIZABLE 标记;

2、具备可序列化性的函数对象将以 SerializedLambda 实例的形式进行序列化。

FLAG_SERIALIZABLELambdaMetafactory 中的一个常量,关于它更多的信息在 altMetafactory() 方法的头部注释有这样一段:

FLAG_SERIALIZABLE的作用

再简单概括下:被 FLAG_SERIALIZABLE 标记而生成的函数对象会实现 Serializable 接口,并且函数对象将有一个名为 writeReplace() 且返回类型为 SerializedLambda 的方法。调用这些函数对象的方法的类中必须有一个名为 $deserializeLambda$ 的方法,如同 SerializedLambda 那样。

这段注释的具体实现可以查看 altMetafactory() 方法的最后几行:

构造InnerClassLambdaMetafactory对象并调用buildCallSite方法

在此构造了 InnerClassLambdaMetafactory 对象并调用其 buildCallSite() 方法,调用的 InnerClassLambdaMetafactory 构造方法中有个名为 isSerializable 的局部变量,其定义如下:

1
boolean isSerializable = ((flags & FLAG_SERIALIZABLE) != 0);

简单来说就是生成的函数对象是否具备可序列化性。

再看 InnerClassLambdaMetafactory 对象的 buildCallSite() 方法,这个方法 首行 调用了名为 spinInnerClass() 的私有方法,这个方法里面有这样一行代码:

调用generateSerializationFriendlyMethods方法

如果 具备可序列化性,那么就调用 generateSerializationFriendlyMethods() 方法,见名识意,这个方法是由来生成序列化方法的,那么 writeReplace() 极有可能就是在此生成的。

查看其源码:

generateSerializationFriendlyMethods方法的部分源码

猜想正确!🎉

小总结

实现了 Serializable 接口的函数式接口生成的函数对象中自动会生成一个名为 writeReplace 且返回值为 SerializedLambda 的方法。

OK,没有显式定义 writeReplace() 方法却能够反射调用 writeReplace() 方法的原因找到了,那 SerializedLambda 又是什么呢?

3.2 SerializedLambda

依旧从注释入手:

SerializedLambda的顶部注释

简单概括下:

第一大段:SerializedLambda 是 Lambda 表达式的序列化形式,它存储了 Lambda 表达式的运行时信息。

第二大段:为确保 Lambda 表达式序列化的正确性,编译器或语言类库可以选用的一种方法是确保 writeReplace() 方法返回 SerializedLambda 实例,而不是进行默认的序列化。

第三大段:SerializedLambda 中有一个名为 readResolve() 的方法,readResolve() 调用“捕获类”中一个名为 $deserializeLambda$(SerializedLambda) 的静态方法(可能是私有的),这个静态方法以当前 SerializedLambda 实例作为第一个参数并返回结果。实现 $deserializeLambda$ 的 Lambda 类负责验证序列化 Lambda 的属性是否与该类实际捕获的 Lambda 一致。可以理解为反序列化。

在高版本 JDK 中此处注释还有第四大段,其大致含义为:序列化和反序列化产生的函数对象的标识信息是不可预测的,因此不同实现类的函数对象、甚至同一个实现类的不同反序列化结果的敏感标识操作(相等性、对象锁、标识哈希码)都是不同的。

说了这么多,重点还是第一大段讲的 SerializedLambda 存储了拥有可序列化性的 Lambda 表达式的运行时信息。

这些运行时信息保存在 SerializedLambda 的成员变量中,各个含义可以参考 SerializedLambda 的全参构造方法的注释:

SerializedLambda全参构造方法的注释

根据注释,整理出如下表格:

成员变量 含义
capturingClass 捕获类,Lambda 表达式出现的所在类
functionalInterfaceClass Lambda 表达式对应的函数式接口名称,以 / 分割
functionalInterfaceMethodName 函数式接口的抽象方法名称
functionalInterfaceMethodSignature 函数式接口的抽象方法的签名(使用了泛型则是泛型擦除后的类型)
implMethodKind 实现方法的 Method Handle 类型
implClass 持有该函数式接口抽象方法的 实现方法 所在的类名称(实现了函数式接口方法的实现类),以 / 分隔
implMethodName 函数式接口抽象方法的 实现方法 名称
implMethodSignature 函数式接口抽象方法的 实现方法 签名
instantiatedMethodType 函数式接口对应的实现类中实现方法的签名
capturedArgs Lambda 表达式捕获的动态参数

有几个要点:

1、implClass 表示 持有 实现方法的 Class 名称,注意并不是函数式接口对应的实现类的名称;

2、上表黑体的 实现方法 与实现类中的实现方法不是同一概念。

SerializedLambda 实例

以一个测试方法为例,并在其 最后一行打上断点,查看 Lambda 表达式与方法引用对应的 SerializedLambda 对象的区别,并比较 SerializedLambda 中成员变量值的差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package indi.mofan;

/**
* @author mofan
* @date 2022/6/9 10:47
*/
public class LambdaTest {
@Test
public void testSerializedLambda() throws Exception {
SFunction<Person, String> methodReferenceFunc = Person::getName;
Method methodReferenceMethod = methodReferenceFunc.getClass().getDeclaredMethod("writeReplace");
methodReferenceMethod.setAccessible(true);
SerializedLambda methodReferenceLambda = (SerializedLambda) methodReferenceMethod.invoke(methodReferenceFunc);


SFunction<Person, String> lambdaFunc = (person) -> person.getName();
Method lambdaMethod = lambdaFunc.getClass().getDeclaredMethod("writeReplace");
lambdaMethod.setAccessible(true);
SerializedLambda lambda = (SerializedLambda) lambdaMethod.invoke(lambdaFunc);

System.out.println(); // 此处断点
}
}

不同SerializedLambda实例之间的区别

方法引用 Person::getName 对应的 SerializedLambda 实例相比于 Lambda 表达式 (person) -> person.getName() 对应的 SerializedLambda 实例 主要 有以下区别:

1、implClass 不同。前者指向 Person 类,后者指向当前测试类;

2、implMethodName 不同。前者指向 getName 方法,后者指向 Lambda 表达式生成的实现类中的对应的实现方法;

3、implMethodSignature 不同。原因同上。

根据上面三个 主要区别,可以使用 implMethodName 的值来判断使用的是方法引用还是 Lambda 表达式。这也是文首 ReflectionUtil.findField() 方法中 implMethodName.startsWith(LAMBDA_PREFIX) 判断的由来。

Lambda 表达式的序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testSerialLambda() throws Exception {
SFunction<Person, String> methodReferenceFunc = Person::getName;
Class<?> methodReferenceClass = serial(methodReferenceFunc);

SFunction<Person, String> lambdaFunc = (person) -> person.getName();
Class<?> lambdaClass = serial(lambdaFunc);

System.out.println(); // 此处断点
}

private Class<?> serial(Serializable serializable) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(serializable);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
return ois.readObject().getClass();
}

testSerialLambda() 方法最后一行断点,查看 methodReferenceClasslambdaClass 的信息:

反序列化方法引用与Lambda表达式产生的Class信息

另一种获取 SerializedLambda 实例的方式

前文中 SerializedLambda 实例都是使用反射调用 writeReplace() 方法来获取的,还可以通过序列化与反序列化的方式来获取 SerializedLambda 实例。

反序列化调用的 ObjectInputStream.readObject() 方法会回调 SerializedLambda.readResolve() 方法,致使最终返回的结果是一个新模板类承载的 Lambda 表达式实例,因此需要想办法中断这个调用以提前返回结果。

方法是构造一个和 java.lang.invoke.SerializedLambda 同名的类(可以不同包名),并且这个类里没有 readResolve() 方法,以便绕过 ObjectStreamClass.classNamesEqual() 方法的判断。

1
2
3
4
5
6
7
8
9
10
/**
* Compares class names for equality, ignoring package names. Returns true
* if class names equal, false otherwise.
*/
private static boolean classNamesEqual(String name1, String name2) {
name1 = name1.substring(name1.lastIndexOf('.') + 1);
name2 = name2.substring(name2.lastIndexOf('.') + 1);
// 只判断类名,并未判断全限定名
return name1.equals(name2);
}

自定义的 SerializedLambda 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package indi.mofan.lambda;

import java.io.Serializable;

/**
* @author mofan
* @date 2022/7/3 18:41
*/
@SuppressWarnings("ALL")
public class SerializedLambda implements Serializable {
private static final long serialVersionUID = 8025925345765570181L;
private Class<?> capturingClass;
private String functionalInterfaceClass;
private String functionalInterfaceMethodName;
private String functionalInterfaceMethodSignature;
private String implClass;
private String implMethodName;
private String implMethodSignature;
private int implMethodKind;
private String instantiatedMethodType;
private Object[] capturedArgs;

public String getCapturingClass() {
return capturingClass.getName().replace('.', '/');
}
public String getFunctionalInterfaceClass() {
return functionalInterfaceClass;
}
public String getFunctionalInterfaceMethodName() {
return functionalInterfaceMethodName;
}
public String getFunctionalInterfaceMethodSignature() {
return functionalInterfaceMethodSignature;
}
public String getImplClass() {
return implClass;
}
public String getImplMethodName() {
return implMethodName;
}
public String getImplMethodSignature() {
return implMethodSignature;
}
public int getImplMethodKind() {
return implMethodKind;
}
public final String getInstantiatedMethodType() {
return instantiatedMethodType;
}
public int getCapturedArgCount() {
return capturedArgs.length;
}
public Object getCapturedArg(int i) {
return capturedArgs[i];
}
}

最后就是一个简单的测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void testCreateSerializedLambdaByAnotherWay() throws Exception {
SFunction<Person, String> function = Person::getName;
indi.mofan.lambda.SerializedLambda serializedLambda = getSerializedLambda(function);

Assert.assertEquals("getName", serializedLambda.getImplMethodName());
}

private static indi.mofan.lambda.SerializedLambda getSerializedLambda(Serializable serializable) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(serializable);
oos.flush();
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())) {
// 重写 resolveClass 方法,半路截胡,使反序列化返回的类型为 indi.mofan.lambda.SerializedLambda
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
Class<?> klass = super.resolveClass(desc);
return klass == java.lang.invoke.SerializedLambda.class ? indi.mofan.lambda.SerializedLambda.class : klass;
}
}) {
return (indi.mofan.lambda.SerializedLambda) ois.readObject();
}
}
}

3.3 隐藏的 $deserializeLambda$ 方法

阅读 SerializedLambda 的顶部注释时知道这个类中有个 readResolve() 方法,它会调用“捕获类”中一个名为 $deserializeLambda$(SerializedLambda) 的静态方法。readResolve() 的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Object readResolve() throws ReflectiveOperationException {
try {
Method deserialize = AccessController.doPrivileged(new PrivilegedExceptionAction<Method>() {
@Override
public Method run() throws Exception {
Method m = capturingClass.getDeclaredMethod("$deserializeLambda$", SerializedLambda.class);
m.setAccessible(true);
return m;
}
});

return deserialize.invoke(null, this);
}
catch (PrivilegedActionException e) {
Exception cause = e.getException();
if (cause instanceof ReflectiveOperationException)
throw (ReflectiveOperationException) cause;
else if (cause instanceof RuntimeException)
throw (RuntimeException) cause;
else
throw new RuntimeException("Exception in SerializedLambda.readResolve", e);
}
}

根据源码可知,是使用反射调用的 $deserializeLambda$ 方法。

根据前文中获取的 SerializedLambda 实例可知,这里的“捕获类”是测试类 indi.mofan.LambdaTest 类,这个类中并没有定义名为 $deserializeLambda$ 的方法,那这真的有吗?测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testGet$deserializeLambda$MethodInfo() throws Exception {
SFunction<Person, String> function = Person::getName;
Method writeReplaceMethod = function.getClass().getDeclaredMethod("writeReplace");
writeReplaceMethod.setAccessible(true);
SerializedLambda serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(function);

// 反射获取“捕获类”对应的 Class 对象
String capturingClassName = serializedLambda.getCapturingClass().replace("/", ".");
Assert.assertEquals(this.getClass().getName(), capturingClassName);
Class<?> capturingClass = Class.forName(capturingClassName);

// 调用 Spring 的方法
ReflectionUtils.doWithMethods(capturingClass, method -> {
Assert.assertEquals("$deserializeLambda$", method.getName());
Assert.assertEquals("private static", Modifier.toString(method.getModifiers()));
Assert.assertEquals(1, method.getParameterCount());
Assert.assertEquals(SerializedLambda.class.getName(), method.getParameterTypes()[0].getName());
Assert.assertEquals(Object.class.getName(), method.getReturnType().getName());
}, method -> Objects.equals(method.getName(), "$deserializeLambda$"));
}

测试方法通过,在“捕获类”中还真存在一个名为 $deserializeLambda$、参数类型为 SerializedLambda、返回值类型为 Object 的私有静态方法。

Spring 的 ReflectionUtils.doWithMethods 方法

ReflectionUtils.doWithMethods() 方法的参数与返回值如下:

1
2
3
public static void doWithMethods(Class<?> clazz, ReflectionUtils.MethodCallback mc, @Nullable ReflectionUtils.MethodFilter mf){
// 省略具体实现
}

doWithMethods() 方法会遍历 clazz 中的所有方法,对这些方法按照过滤器 mf 进行筛选,过滤得到的方法都将回调 mc


到此,SerializedLambda 类和 writeReplace() 方法的作用都得到了解答,解决了两个最大的问题后并不意味着就结束了,其中依然存在着很多细节。比如:

  • 函数式接口对应的实现类是什么?
  • ReflectionUtil 中缓存 CACHE 为什么要以 SFunction 作为 key,这有意义吗?
  • LambdaUtil.getSerializedLambda()aClass.isSynthetic() 是用来判断什么的?
  • ReflectionUtil.findField() 中使用到的 Introspector 类是什么?
  • Debug 查看 SerializedLambda 对象的信息时,为什么许多信息都以 / 分割,而不是 .
  • LambdaUtil.getSerializedLambda() 中获取 writeReplaceMethod 对象时为什么使用了 getDeclaredMethod() 方法,而不是 getMethod()

4. 其他细节补充

4.1 函数式接口的实现类

前文说到,程序中 Lambda 表达式或方法引用使用的函数式接口最终必定也是一个实现类的实例对象,那这实现类是什么样子的?有没有什么方式能看到?

还真有,在 Java8 中可以通过如下硬编码形式的属性配置将函数式接口动态生成的 class 文件保存到磁盘上:

1
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");

由于 Java9 引进的模块化,后续版本的 JDK 只能通过 JVM 参数指定:

1
2
3
4
5
# JDK 11 中指定的参数
-Djdk.internal.lambda.dumpProxyClasses

# JDK 21 中指定的参数
-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles

显示函数式接口生成的实现类

根据上方的图片可以看出, 代码中多次编写的同一个方法引用会生成多个实现类,它们的实例对象也不会是同一个。

在动态生成的 Class 中还可以看到 writeReplace() 方法:

方法引用动态生成的Class

既然一个方法创建一个实现类,实现类又对应不同的对象,那么 ReflectionUtil 中将 SFunction 作为缓存的 key 还有意义吗?

答案是肯定的。因为 同一方法中 定义 的 Lambda 表达式或方法引用只会动态创建一次实现类并只实例化一次, 而被多次调用时就可以利用缓存提供性能。

或许还不明白,那么请看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testCallSFunction() {
SFunction<Person, String> function1 = Person::getName;
SFunction<Person, String> function2 = Person::getName;

Assert.assertNotSame(function1, function2);

List<SFunction<Person, String>> list = new ArrayList<>();
for (int i = 0; i < 3; i++) {
SFunction<Person, String> function = Person::getName;
System.out.println(function);
list.add(function);
}
Assert.assertSame(list.get(0), list.get(1));
Assert.assertSame(list.get(1), list.get(2));
Assert.assertSame(list.get(2), list.get(0));
}

或许你以为上述代码将创建 5 个实现类,其实只会创建 3 个。for 循环中的定义的方法引用只会创建 1 个实现类,而不是 3 个,因为它只被定义了一次。这种情况下,不就可以走缓存了吗?

那缓存又为什么不以 SFunctionClass 为 key,还要以这种让人疑惑的 SFunction 实例对象为 key 呢?

已经知道无论方法被调用多少次,方法内的方法引用对象始终是同一个,如果采用其 Class 做为缓存 key,每次查询缓存时都需要调用 native 方法getClass()获取其 Class,而这也是一种资源损耗。

4.2 synthetic class

Class 类中有这样一个私有常量和方法:

1
2
3
4
5
6
7
8
9
10
11
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {

private static final int SYNTHETIC = 0x00001000;

public boolean isSynthetic() {
return (getModifiers() & SYNTHETIC) != 0;
}
}

isSynthetic() 用于判断一个 Class 是否是合成的 Class。那什么是合成的 Class?

非基本类型,即复合类型(引用类型)就是合成的 Class?非也。

合成的 Class 是指 由 JVM 生成,并且在源代码中没有符合的结构将被标记为合成的。不包括默认构造器、类初始化方法(<init>())、枚举的 values 和 valueOf 方法。

我们已经知道 Lambda 表达式或方法引用在程序运行时会动态生成的 Class,这些 Class 就是合成的。

既然都是合成的,那为什么还要使用 isSynthetic() 进行判断呢?

在 Java8 版本以前,还没有 Lambda 表达式时,经常用匿名内部类代替 Lambda,为了排除这种情况,就需要使用 isSynthetic() 进行判断。比如:

1
2
3
4
5
6
/**
* @author mofan
* @date 2022/6/7 17:37
*/
public interface SSupplier<T> extends Supplier<T>, Serializable {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testGetImplClass() throws Exception {

Person person = new Person();
SSupplier<String> consumer = person::getName;
Assert.assertEquals("indi.mofan.domain.Person", getImplClass(consumer));

SSupplier<String> supplier = new SSupplier<String>() {
@Override
public String get() {
return person.getName();
}
};
// 空字符串
Assert.assertTrue(getImplClass(supplier).isEmpty());
}

4.3 Introspector

Introspectorjava.beans 包下的一个类,它为目标 JavaBean 提供了一种了解原类方法、属性和事件等信息的标准方法。简单来说,可以通过 Introspector 构建一个 BeanInfo 对象,而这个 BeanInfo 对象中包含了目标 JavaBean 中的属性、方法和事件等描述信息,除了获取这些信息外,还可以使用这个 BeanInfo 对象对目标 JavaBean 对象进行相关操作。

什么是 JavaBean?

JavaBean 就是一种特殊的、可重用的 Java 类。

菜鸟教程关于 JavaBean 是这么描述的:

JavaBean 是特殊的 Java 类,使用 Java 语言书写,并且遵守 JavaBean API 规范。JavaBean 与其它 Java 类相比而言独一无二的特征:

  • 提供一个默认的无参构造函数。
  • 需要被序列化并且实现了 Serializable 接口。
  • 可能有一系列可读写属性。
  • 可能有一系列的 getter 或 setter 方法。

百度百科关于 JavaBean 是这么描述的:

JavaBean 是一种 Java 语言写成的可重用组件。为写成 JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 通过提供符合一致性设计模式的公共方法将内部域暴露成员属性,set 和 get 方法获取。众所周知,属性名称符合这种模式,其他 Java 类可以通过自省机制(反射机制)发现和操作这些 JavaBean 的属性。

JavaBean 是一种可重用的 Java 组件,它可以被 Applet、Servlet、JSP 等 Java 应用程序调用.也可以可视化地被 Java 开发工具使用。它包含属性(Properties)、方法(Methods)、事件(Events)等特性。

维基百科关于 JavaBean 是这么说的:

JavaBeans 是 Java 中一种特殊的类,可以将多个对象封装到一个对象(bean)中。特点是可序列化,提供 public 无参构造器,提供 getter 方法和 setter 方法访问对象的属性。名称中的“Bean”是用于 Java 的可重用软件组件的惯用叫法。

比如,下述的 Person 就是一个 JavaBean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author mofan
* @date 2022/3/29 12:24
*/
public class Person implements Serializable {
private static final long serialVersionUID = 8597585718927776100L;

private String name;
private String age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAge() {
return age;
}

public void setAge(String age) {
this.age = age;
}
}

简单的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
public void testIntrospector() throws Exception {
// 获取整个 bean 信息
// BeanInfo personBeanInfo = Introspector.getBeanInfo(Person.class);
// 检索到 Object 时停止,可以检索到 JavaBean 的任意父类时停止
BeanInfo beanInfo = Introspector.getBeanInfo(Person.class, Object.class);

// 获取属性信息
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
List<String> beanFieldInfo = Arrays.stream(propertyDescriptors).map(PropertyDescriptor::getName)
.collect(Collectors.toList());
List<String> classFieldInfo = Arrays.stream(Person.class.getDeclaredFields()).map(Field::getName).collect(Collectors.toList());
Assert.assertTrue(classFieldInfo.containsAll(beanFieldInfo));
Assert.assertEquals(2, beanFieldInfo.size());

// 获取 bean 中 public 的方法
MethodDescriptor[] methodInfo = beanInfo.getMethodDescriptors();
Assert.assertEquals(4, methodInfo.length);


// 获取属性的值
Person person = new Person();
person.setName("mofan");
String propertyName = "name";
PropertyDescriptor nameProperty = new PropertyDescriptor(propertyName, Person.class);
Assert.assertEquals("mofan", nameProperty.getReadMethod().invoke(person));
// 修改属性的值
nameProperty.getWriteMethod().invoke(person, "TestName");
Assert.assertEquals("TestName", nameProperty.getReadMethod().invoke(person));
// 原对象也有影响
Assert.assertEquals("TestName", person.getName());
}

通常会使用以下两种方式获取 BeanInfo 对象:

1
2
3
public static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException

public static BeanInfo getBeanInfo(Class<?> beanClass, Class<?> stopClass) throws IntrospectionException

更建议使用第二种方式来获取 BeanInfo 对象。采用这种方式,将检索到 stopClass 时停止。比如将 stopClass 设置为 Object.class,这样的话可以规避 Object 类中的信息,进而得到更加纯净的目标 JavaBean 信息。

比如:

1
2
3
4
5
6
7
8
@Test
public void testGetAllBeanInfo() throws Exception {
BeanInfo beanInfo = Introspector.getBeanInfo(Person.class);

// 获取所有属性
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
Assert.assertEquals(3, propertyDescriptors.length);
}

Person 中明明只有 nameage 两个成员变量,为什么获取到的属性数量是 3 呢?Debug 一下:

获取到的class属性

居然有一个 name 为 class 的属性,这是为什么?Person 类中没有,Object 类中也没有,它是哪来的?

要解决这个问题就得说说 Java 类中成员变量 Fields 和属性 Properties 的区别了。

Java 类中成员变量 Fields 和属性 Properties 的区别

或许你认为这两个表示同一个意思,其实并不是,以 Person 为例:

Person的Fields和Properties

成员变量 Fields 很好理解,就是:

1
2
private String name;
private String age;

但属性 Properties 就不是这样了,它指的是 getter/setter 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAge() {
return age;
}

public void setAge(String age) {
this.age = age;
}

使用 IDEA 对类结构进行显示时,在左侧也能清晰地显示成员变量和属性的区别。

属性 Properties 的官方定义:属性是指 get 或者 set 方法名去掉 get 或者 set 后,把剩余的部分首字母改为小写后,即为这个类的属性。 比如,getName 转换为 namegetname 转换为 namegetURL 转换为 URL

Object 类中有名为 getClass(),因此 class 也是一个属性。正因如此,更推荐使用以下方式获取 BeanInfo 以得到纯净的 JavaBean 信息:

1
public static BeanInfo getBeanInfo(Class<?> beanClass, Class<?> stopClass) throws IntrospectionException

Fields 和 Properties 一般来说时相等的,但也时常会存在不相等的情况,针对这些情况,常用反射来获取 Fields,使用 Introspector 来获取 Properties。比如:

1
2
3
4
5
6
7
8
9
@Test
public void testGetFieldsAndProperties() throws Exception {
// 获取 Fields
Field[] fields = Person.class.getDeclaredFields();

// 获取 Properties
BeanInfo beanInfo = Introspector.getBeanInfo(Person.class, Object.class);
PropertyDescriptor[] properties = beanInfo.getPropertyDescriptors();
}

还需要注意,由于在 Person 中定义了 serialVersionUID,这也是一个 Field:

名为serialVersionUID的Field

内省 Introspector 和反射 Reflection 的区别

内省 Introspector 和反射 Reflection 很相似,就连使用方法也是如此,Introspector 更多的使用方法自行探索,在此梳理下它们两者的区别。

反射是在运行时获取类的所有信息,包括成员变量、成员方法、构造器等,并且还可以操纵修改对象的字段值、方法、构造器等内容。反射可以理解为类“照镜子”后得到的信息,就像我们现实中照镜子一样,类“照镜子”后得到的信息 必定是正确的。

内省基于反射实现,常用于操作 JavaBean,基于 JavaBean 的规范对 Bean 信息进行解析,依据于类的 GetterSetter 方法,获取到类的描述符。可以理解为“类的反省”,也和现实中的反省一样,“类的反省”不一定是正确的。如果一个类中的属性没有SetterGetter方法,无法使用内省。

可能导致的内存溢出

如果框架或者程序用到了 Introspector,就相当于启用了一个系统级别的缓存,这个缓存会存放一些曾加载并分析过的 JavaBean 的引用。当 Web 服务器关闭时,由于这个缓存中存放着的 JavaBean 的引用,所以垃圾回收器不能对 Web 容器中的 JavaBean 对象进行回收,导致内存越来越大。

清除 Introspector 缓存的唯一方式是刷新整个缓存缓冲区,这是因为 JDK 没法判断哪些是属于当前的应用的引用,而刷新整个 Introspector 缓存缓冲区又会导致把服务器上所有应用的 Introspector 缓存都删掉。对此 Spring 提供了 org.springframework.web.util.IntrospectorCleanupListener 来解决这个问题,当某个 Web 服务器停止时,就会清理这个服务器下的 Introspector 缓存,使那些 JavaBean 能被垃圾回收器正确回收。

也就是说 JDK 的 Introspector 缓存管理是有一定缺陷的。但如果在 Spring 体系中使用则不会出现这种问题,在 Spring 体系下 Introspector 缓存的管理移交给了 Spring 自身而不是 JDK(或者在 Web 容器销毁后完全不管)。

在 Spring 体系中,为了防止 JDK 对 Introspector 的缓存无法被垃圾回收机制回收导致内存溢出,主要的操作除了可以通过配置 IntrospectorCleanupListener 预防外,还可以通过 CachedIntrospectionResults 类自行管理 Introspector 中的缓存(这种方式才是优雅的方式,这可以避免刷新整个 Introspector 的缓存缓冲区而导致其他应用的 Introspector 也被清空)。

在 SpringBoot 刷新上下文的方法 AbstractApplicationContext#refresh() 中的 finally 代码块中 AbstractApplicationContext#resetCommonCaches(); 方法里调用到的 CachedIntrospectionResults#clearClassLoader(getClassLoader()) 方法就是清理指定的 ClassLoader 下的所有 Introspector 中的缓存的引用。

AbstractApplicationContext中的resetCommonCaches方法

resetCommonCaches() 方法的源码如下:

1
2
3
4
5
6
7
protected void resetCommonCaches() {
ReflectionUtils.clearCache();
AnnotationUtils.clearCache();
ResolvableType.clearCache();
// 清除指定 ClassLoader 下所有 Introspector 的缓存的引用
CachedIntrospectionResults.clearClassLoader(this.getClassLoader());
}

4.4 Java 描述符

当我们 Debug 查看 SerializedLambda 对象的信息时,许多信息都以 / 分割,而不是印象中的 .。比如:

不同SerializedLambda实例之间的区别

具体原因总结下来就四个字:历史原因。重点是看一下它的规则是怎么样的。

类型描述符

Java 中的类型分为基本类型和引用类型,基本类型的描述符是单个字母,具体内容可以查看 sun.invoke.util.Wrapper 中的内容:

sun.invoke.util.Wrapper枚举项

根据上图所示,boolean 的类型描述符是 Zbyte 的类型描述符是 B,针对基本类型,可以得到如下对照表:

基本类型 类型描述符
boolean Z
byte B
short S
char C
int I
long J
float F
double D
void V

注意: java.lang.Object 的类型描述符并不是 L,而是 Ljava/lang/Object;

基本类型有了,再来看看引用类型。引用类型的类型描述符格式是以 L 开头,紧跟上以 / 分割的类全限定名,最后以 ; 结尾,就像前面说的 java.lang.Object 的类型描述符一样。一个数组类型的描述符是一个 [ 后跟上该数组元素类型的描述符,并且是几维数组,就有几个 [ 在开头。针对引用类型,可以得到如下对照参考表:

基本类型 类型描述符
Object Ljava/lang/Object;
int[] [I
Object[][] [[Ljava/lang/Object;
String Ljava/lang/String;

方法描述符

方法描述符的格式为:

1
(参数类型描述符们)返回值类型描述符

如果有多个参数,按照参数列表依次写出它们的类型描述符即可,之间也不需要任何分隔符。比如:

方法的声明 方法描述符
void m(int i, float f) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i,String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I)Ljava/lang/Object;

获取描述符信息

要获取描述符信息,可以使用 ASM 中的方法,首先导入依赖:

1
2
3
4
5
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.3</version>
</dependency>

简单测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testGetModifierInfo() throws Exception {
Assert.assertEquals("java/lang/String", Type.getInternalName(String.class));
Assert.assertEquals("java/util/Map", Type.getInternalName(Map.class));

Assert.assertEquals("Ljava/lang/String;", Type.getDescriptor(String.class));

Assert.assertEquals("I", Type.INT_TYPE.getDescriptor());

Method setNameMethod = Person.class.getDeclaredMethod("setName", String.class);
Assert.assertEquals("(Ljava/lang/String;)V", Type.getMethodDescriptor(setNameMethod));
}

4.5 Declared 修饰的方法

LambdaUtil.getSerializedLambda() 方法中使用了 getDeclaredMethod() 方法,而不是 getMethod() 方法,并且查看 Class 类中的方法,发现有很多类似这样的一组方法,一个被 Declared 修饰,而另一个又没被修饰。那么它们之间有什么区别呢?

通过一个测试类来讲解他们之间的关系。先定义一个父类:

1
2
3
4
5
6
7
8
9
10
/**
* @author mofan
* @date 2022/7/4 18:15
*/
@Getter
@Setter
public class Parent {
private String privateParentField;
public String publicParentField;
}

然后定义一个接口:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author mofan
* @date 2022/7/4 18:17
*/
public interface SimpleInterface {
/**
* 接口中的变量都是常量
*/
String CONSTANT = "constant";

int add(int a, int b);
}

再定义一个子类,这个子类继承父类并实现接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mofan
* @date 2022/7/4 18:16
*/
@Getter
@Setter
public class Child extends Parent implements SimpleInterface {
private String privateChildField;
public String publicChildField;

@Override
public int add(int a, int b) {
return a + b;
}
}

最后利用子类和父类的 Class 进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Test
public void testDeclared() {
Class<Parent> parentClass = Parent.class;
Class<Child> childClass = Child.class;

// 当前类及其父类中所有 public 的字段
Assert.assertEquals(1, parentClass.getFields().length);
Assert.assertTrue(Arrays.stream(parentClass.getFields()).allMatch(i -> "publicParentField".equals(i.getName())));
// 当前类中所有的字段
List<String> parentFieldNameList = Arrays.asList("privateParentField", "publicParentField");
Assert.assertEquals(parentFieldNameList.size(), parentClass.getDeclaredFields().length);
Assert.assertTrue(Arrays.stream(parentClass.getDeclaredFields()).map(Field::getName).allMatch(parentFieldNameList::contains));

// 当前类及其父类中所有 public 的字段
List<String> childPublicFieldNameList = Arrays.asList("publicChildField", "publicParentField", "CONSTANT");
Assert.assertEquals(childPublicFieldNameList.size(), childClass.getFields().length);
Assert.assertTrue(Arrays.stream(childClass.getFields()).map(Field::getName).allMatch(childPublicFieldNameList::contains));
// 当前类中所有的字段(实现的接口中的常量不在其中)
List<String> childFiledNameList = Arrays.asList("privateChildField", "publicChildField");
Assert.assertEquals(childFiledNameList.size(), childClass.getDeclaredFields().length);
Assert.assertTrue(Arrays.stream(childClass.getDeclaredFields()).map(Field::getName).allMatch(childFiledNameList::contains));

// 当前类及其父类中所有 public 的方法(因此包含 Object 类中 public 的方法)
List<String> parentPublicMethodNameList = Arrays.asList("setPrivateParentField", "getPublicParentField",
"getPrivateParentField", "setPublicParentField");
Assert.assertTrue(parentClass.getMethods().length > 4);
Assert.assertEquals(Object.class.getMethods().length + 4, parentClass.getMethods().length);
Assert.assertTrue(Arrays.stream(parentClass.getMethods()).map(Method::getName).collect(Collectors.toList()).containsAll(parentPublicMethodNameList));
// 当前类中所有的方法
Assert.assertEquals(parentPublicMethodNameList.size(), parentClass.getDeclaredMethods().length);
Assert.assertTrue(Arrays.stream(parentClass.getDeclaredMethods()).map(Method::getName).allMatch(parentPublicMethodNameList::contains));

// 当前类及其父类中所有 public 的方法
List<String> childPublicMethodNameList = Arrays.asList("add", "setPublicChildField", "getPublicChildField",
"setPrivateChildField", "getPrivateChildField");
Assert.assertTrue(childClass.getMethods().length > childPublicMethodNameList.size());
Assert.assertEquals(parentClass.getMethods().length + childPublicMethodNameList.size(), childClass.getMethods().length);
Assert.assertTrue(Arrays.stream(childClass.getMethods()).map(Method::getName).collect(Collectors.toList()).containsAll(childPublicMethodNameList));

// 当前类中所有的方法
Assert.assertEquals(childPublicMethodNameList.size(), childClass.getDeclaredMethods().length);
Assert.assertTrue(Arrays.stream(childClass.getDeclaredMethods()).map(Method::getName).allMatch(childPublicMethodNameList::contains));
}

上述测试方法能够通过,对此不难得出:

1、getFields() 方法可以获取当前类及其父类中所有 public 的成员变量;getDeclaredFields() 方法可以获取当前类中所有的成员变量。

2、getMethods() 方法可以获取当前类及其父类中所有 public 的方法;getDeclaredMethods() 方法可以获取当前类中所有的方法。

5. 结语

以上就是《Lambda 与序列化》一文中的全部内容,本文不仅仅讲述了如何通过 Java8 的方法引用来获取字段名称,还围绕着具体实现进行展开,介绍了 Java 的序列化、Lambda 表达式的序列化以及具体实现中的其他细节。

最近这一个多月工作都比较忙,经常加班,就连周六都不例外,因此本文从构思到最终完成竟然用了足足一个月,但幸运的是过程中学到了很多不曾了解的知识点,也算是收获颇多。依我看来,这篇文章或许能成为我今年的代表作,当然也希望在今年剩下的几个月中能写出深度、广度跟本文一样,甚至超越本文的篇章。

最后向本文所有参考文章的作者致以诚挚的谢意。