JDK中Lambda表达式的序列化与SerializedLambda的巧妙使用
前提
神奇的Lambda表达式序列化
之前在看
Lambda
表达式源码实现的时候没有细看
LambdaMetafactory
的注释,这个类顶部大量注释中其中有一段如下:
简单翻译一下就是:可序列化特性。一般情况下,生成的函数对象(这里应该是特指基于
Lambda
表达式实现的特殊函数对象)不需要支持序列化特性。如果需要支持该特性,
FLAG_SERIALIZABLE
(
LambdaMetafactory
的一个静态整型属性,值为
1 << 0
)可以用来表示函数对象是序列化的。一旦使用了支持序列化特性的函数对象,那么它们以
SerializedLambda
类的形式序列化,这些
SerializedLambda
实例需要额外的”捕获类”的协助(捕获类,如
MethodHandles.Lookup
的
caller
参数所描述),详细信息参阅
SerializedLambda
。
在
LambdaMetafactory
的注释中再搜索一下
FLAG_SERIALIZABLE
,可以看到这段注释:
大意为:设置了
FLAG_SERIALIZABLE
标记后生成的函数对象实例会实现
Serializable
接口,并且会存在一个名字为
writeReplace
的方法,该方法的返回值类型为
SerializedLambda
。调用这些函数对象的方法(前面提到的”捕获类”)的调用者必须存在一个名字为
$deserializeLambda$
的方法,如
SerializedLambda
类所描述。
最后看
SerializedLambda
的描述,注释有四大段,这里贴出并且每小段提取核心信息:
各个段落大意如下:
SerializedLambda
是
Lambda
表达式的序列化形式,这类存储了
Lambda
表达式的运行时信息
Lambda
表达式的序列化实现正确性,编译器或者语言类库可以选用的一种方式是确保
writeReplace
方法返回一个
SerializedLambda
实例
SerializedLambda
提供一个
readResolve
方法,其职能类似于调用”捕获类”中静态方法
$deserializeLambda$(SerializedLambda)
并且把自身实例作为入参,该过程理解为反序列化过程
System.identityHashCode()
、对象锁定等等)是不可预测的
最终的结论就是:如果一个函数式接口实现了
Serializable
接口,那么它的实例就会自动生成了一个返回
SerializedLambda
实例的
writeReplace
方法,可以从
SerializedLambda
实例中获取到这个函数式接口的运行时信息。这些运行时信息就是
SerializedLambda
的属性:
capturingClass
“捕获类”,当前的
Lambda
表达式出现的所在类
functionalInterfaceClass
名称,并且以”/“分隔,返回的
Lambda
对象的静态类型
functionalInterfaceMethodName
函数式接口方法名称
functionalInterfaceMethodSignature
函数式接口方法签名(其实是参数类型和返回值类型,如果使用了泛型则是擦除后的类型)
implClass
名称,并且以”/“分隔,持有该函数式接口方法的实现方法的类型(实现了函数式接口方法的实现类)
implMethodName
函数式接口方法的实现方法名称
implMethodSignature
函数式接口方法的实现方法的方法签名(实是参数类型和返回值类型)
instantiatedMethodType
用实例类型变量替换后的函数式接口类型
capturedArgs
Lambda
捕获的动态参数
implMethodKind
实现方法的
MethodHandle
类型
举个实际的例子,定义一个实现了
Serializable
的函数式接口并且调用它:
public class App { |
执行的
DEBUG
信息如下:
这样就能获取到函数式接口实例在调用方法时候的调用点运行时信息,甚至连泛型参数擦除前的类型都能拿到,那么就可以衍生出很多技巧。例如:
public class ConditionApp { |
很多人会担心反射调用的性能,其实在高版本的JDK,反射性能已经大幅度优化,十分逼近直接调用的性能,更何况有些场景是少量反射调用场景,可以放心使用。
前面花大量篇幅展示了
SerializedLambda
的功能和使用,接着看
Lambda
表达式的序列化与反序列化:
public class SerializedLambdaApp { |
结果如下图:
Lambda表达式序列化原理
关于
Lambda
表达式序列化的原理,可以直接参考
ObjectStreamClass
、
ObjectOutputStream
和
ObjectInputStream
的源码,这里直接说结论:
Serializable
接口
writeReplace
方法,则直接基于传入的实例反射调用此方法得到的返回值类型作为序列化的目标类型,对于
Lambda
表达式就是
SerializedLambda
类型
readResolve
,刚好前面提到
SerializedLambda
也存在同名的私有方法
Lambda
表达式的实现类型是
VM
生成的模板类,从结果上观察,序列化前的实例和反序列化后得到的实例属于不同的模板类,对于前一小节的例子某次运行的结果中序列化前的模板类为
club.throwable.lambda.SerializedLambdaApp$$Lambda$14/0x0000000800065840
,反序列化后的模板类为
club.throwable.lambda.SerializedLambdaApp$$Lambda$26/0x00000008000a4040
ObjectStreamClass是序列化和反序列化实现的类描述符,关于对象序列化和反序列化的类描述信息可以从这个类里面的成员属性找到,例如这里提到的writeReplace和readResolve方法
图形化的过程如下:
获取SerializedLambda的方式
通过前面的分析,得知有两种方式可以获取
Lambda
表达式的
SerializedLambda
实例:
Lambda
表达式实例和
Lambda
表达式的模板类反射调用
writeReplace
方法,得到的返回值就是
SerializedLambda
实例
SerializedLambda
实例
基于这两种方式可以分别编写例子,例如反射方式如下:
// 反射方式 |
序列化和反序列方式会稍微复杂,因为
ObjectInputStream.readObject()
方法会最终回调
SerializedLambda.readResolve()
方法,导致返回的结果是一个新模板类承载的
Lambda
表达式实例,所以这里需要想办法中断这个调用提前返回结果,方案是构造一个和
SerializedLambda
相似但是不存在
readResolve()
方法的
影子类型
:
package cn.vlts; |
被遗忘的
$deserializeLambda$
方法
前文提到,
Lambda
表达式实例反序列化的时候会调用
java.lang.invoke.SerializedLambda.readResolve()
方法,神奇的是,此方法源码如下:
private Object readResolve() throws ReflectiveOperationException { |
看起来就是”捕获类”中存在一个这样的静态方法:
class CapturingClass { |
可以尝试检索”捕获类”中的方法列表:
public class CapturingClassApp { |
果真是存在一个和之前提到的
java.lang.invoke.SerializedLambda
注释描述一致的”捕获类”的
SerializedLambda
实例转化为
Lambda
表达式实例的方法,因为搜索多处地方都没发现此方法的踪迹,猜测
$deserializeLambda$
是方法由
VM
生成,并且只能通过反射的方法调用,算是一个隐藏得比较深的技巧。
小结
JDK
中的
Lambda
表达式功能已经发布很多年了,想不到这么多年后的今天才弄清楚其序列化和反序列化方式,虽然这不是一个复杂的问题,但算是最近一段时间看到的比较有意思的一个知识点。
参考资料: