概述
Java官方文档强烈不建议序列化Lambda表达式。原因在于Lambda会生成合成构造(synthetic constructs),这些构造存在几个潜在问题:
- 源代码中没有对应的构造
- 不同Java编译器实现存在差异
- 与不同JRE实现存在兼容性问题
但实际开发中,序列化Lambda有时是必要的。本文将深入讲解如何序列化Lambda表达式及其底层实现机制。
Lambda与序列化
使用Java序列化时,对象的类和非静态字段都必须可序列化,否则会抛出NotSerializableException
。序列化Lambda时,必须确保其目标类型和捕获参数都是可序列化的。
失败的Lambda序列化示例
以下代码使用Runnable
接口构造Lambda表达式:
public class NotSerializableLambdaExpression {
public static Object getLambdaExpressionObject() {
Runnable r = () -> System.out.println("please serialize this message");
return r;
}
}
尝试序列化这个Runnable
对象时会抛出NotSerializableException
。原因在于:
当JVM遇到Lambda表达式时,会使用内置ASM工具生成一个内部类。我们可以通过命令行参数-Djdk.internal.lambda.dumpProxyClasses=<dump directory>
导出生成的内部类(注意:目标目录最好为空,避免第三方库生成的类干扰)。
反编译生成的内部类后发现:
- 该类仅实现了
Runnable
接口(Lambda的目标类型) run
方法调用编译器生成的NotSerializableLambdaExpression.lambda$getLambdaExpressionObject$0
方法
由于生成的内部类未实现Serializable
接口,导致Lambda不可序列化。
如何序列化Lambda
解决方案:使用交集类型(intersection type)将Lambda表达式强制转换为同时实现函数式接口和Serializable
的类型。例如:
Runnable r = (Runnable & Serializable) () -> System.out.println("please serialize this message");
这样序列化就能成功。但频繁使用会产生样板代码,更优雅的方式是定义新接口:
interface SerializableRunnable extends Runnable, Serializable {
}
然后直接使用:
SerializableRunnable obj = () -> System.out.println("please serialize this message");
但要注意:不能捕获不可序列化的参数。例如:
interface SerializableConsumer<T> extends Consumer<T>, Serializable {
}
// 错误示例:捕获了不可序列化的System.out
SerializableConsumer<String> obj = System.out::println; // 会抛出NotSerializableException
因为System.out
是PrintStream
类型,而PrintStream
不可序列化。
底层实现机制
当我们使用交集类型后,底层发生了什么?以下面代码为例:
public class SerializableLambdaExpression {
public static Object getLambdaExpressionObject() {
Runnable r = (Runnable & Serializable) () -> System.out.println("please serialize this message");
return r;
}
}
编译后的类文件
使用javap -v -p
查看编译后的类文件,会发现编译器生成了$deserializeLambda$
方法:
private static Object $deserializeLambda$(SerializedLambda lambda) {
// 验证Lambda元数据
if (lambda.getImplMethodName().equals("lambda$getLambdaExpressionObject$36ab28bd$1")
&& lambda.getFunctionalInterfaceClass().equals("java/lang/Runnable")
&& lambda.getFunctionalInterfaceMethodName().equals("run")
// 其他验证...
) {
return SerializableLambdaExpression::lambda$getLambdaExpressionObject$36ab28bd$1;
}
throw new IllegalArgumentException("Invalid lambda");
}
该方法的核心职责:
- 验证
SerializedLambda
对象的元数据 - 若验证通过,调用实际Lambda方法创建实例
- 否则抛出
IllegalArgumentException
生成的内部类
导出生成的内部类后发现:
final class SerializableLambdaExpression$$Lambda$1 implements Runnable, Serializable {
private SerializableLambdaExpression$$Lambda$1() {}
public void run() {
SerializableLambdaExpression.lambda$getLambdaExpressionObject$36ab28bd$1();
}
private Object writeReplace() {
return new SerializedLambda(
SerializableLambdaExpression.class,
"java/lang/Runnable",
"run",
"()V",
"lambda$getLambdaExpressionObject$36ab28bd$1",
"(Ljava/lang/invoke/SerializedLambda;)Ljava/lang/Object;",
false,
0
);
}
}
关键点:
- 同时实现了
Runnable
和Serializable
- 提供
writeReplace()
方法返回SerializedLambda
实例
序列化文件内容
序列化后的二进制文件包含:
- 魔数
AC ED
(Base64为rO0
) - 流版本号
00 05
SerializedLambda
类数据(包含10个字段)
这些字段记录了Lambda的完整元数据,如:
- 捕获类名
- 函数式接口信息
- 实现方法名
- 签名等
完整流程解析
序列化与反序列化的完整流程:
序列化过程:
ObjectOutputStream
发现对象有writeReplace()
方法- 调用该方法获取
SerializedLambda
实例 - 序列化
SerializedLambda
而非原始对象
反序列化过程:
ObjectInputStream
读取SerializedLambda
实例- 调用
SerializedLambda.readResolve()
方法 readResolve()
调用捕获类的$deserializeLambda$()
方法- 重建Lambda对象
SerializedLambda
类是整个序列化机制的核心枢纽,它连接了编译时生成的$deserializeLambda$
方法和运行时生成的内部类。
总结
本文通过失败的序列化案例分析了Lambda不可序列化的原因,给出了两种解决方案:
- 强制类型转换为交集类型(简单粗暴)
- 定义专用接口(更优雅)
同时深入探讨了底层实现机制:
- 编译器生成
$deserializeLambda$
方法 - 运行时生成实现
Serializable
的内部类 SerializedLambda
作为序列化载体
实际开发中,序列化Lambda要特别注意避免捕获不可序列化的外部变量,否则会踩坑。完整示例代码可参考GitHub仓库。