概述

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.outPrintStream类型,而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");
}

该方法的核心职责:

  1. 验证SerializedLambda对象的元数据
  2. 若验证通过,调用实际Lambda方法创建实例
  3. 否则抛出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
        );
    }
}

关键点:

  • 同时实现了RunnableSerializable
  • 提供writeReplace()方法返回SerializedLambda实例

序列化文件内容

序列化后的二进制文件包含:

  • 魔数AC ED(Base64为rO0
  • 流版本号00 05
  • SerializedLambda类数据(包含10个字段)

这些字段记录了Lambda的完整元数据,如:

  • 捕获类名
  • 函数式接口信息
  • 实现方法名
  • 签名等

完整流程解析

序列化与反序列化的完整流程:

  1. 序列化过程

    • ObjectOutputStream发现对象有writeReplace()方法
    • 调用该方法获取SerializedLambda实例
    • 序列化SerializedLambda而非原始对象
  2. 反序列化过程

    • ObjectInputStream读取SerializedLambda实例
    • 调用SerializedLambda.readResolve()方法
    • readResolve()调用捕获类的$deserializeLambda$()方法
    • 重建Lambda对象

SerializedLambda类是整个序列化机制的核心枢纽,它连接了编译时生成的$deserializeLambda$方法和运行时生成的内部类。

总结

本文通过失败的序列化案例分析了Lambda不可序列化的原因,给出了两种解决方案:

  1. 强制类型转换为交集类型(简单粗暴)
  2. 定义专用接口(更优雅)

同时深入探讨了底层实现机制:

  • 编译器生成$deserializeLambda$方法
  • 运行时生成实现Serializable的内部类
  • SerializedLambda作为序列化载体

实际开发中,序列化Lambda要特别注意避免捕获不可序列化的外部变量,否则会踩坑。完整示例代码可参考GitHub仓库


原始标题:Serialize a Lambda in Java | Baeldung