1. 概要

一般的に、 Javaドキュメントは、ラムダ式をシリアル化することを強くお勧めしません。 これは、ラムダ式が合成コンストラクトを生成するためです。 また、これらの合成構造にはいくつかの潜在的な問題があります。ソースコードに対応する構造がない、さまざまなJavaコンパイラ実装間でばらつきがある、さまざまなJRE実装との互換性の問題です。 ただし、ラムダのシリアル化が必要な場合があります。

このチュートリアルでは、ラムダ式とその基になるメカニズムをシリアル化する方法を説明します。

2. Lambda and Serialization

Java Serialization を使用してオブジェクトをシリアル化または逆シリアル化する場合、そのクラスフィールドと非静的フィールドはすべてシリアル化可能である必要があります。 そうしないと、NotSerializableExceptionが発生します。 同様に、ラムダ式をシリアル化するときは、そのターゲットタイプとキャプチャ引数がシリアル化可能であることを確認する必要があります

2.1. 失敗したLambdaシリアル化

ソースファイルで、Runnableインターフェイスを使用してラムダ式を作成しましょう。

public class NotSerializableLambdaExpression {
    public static Object getLambdaExpressionObject() {
        Runnable r = () -> System.out.println("please serialize this message");
        return r;
    }
}

Runnable オブジェクトをシリアル化しようとすると、NotSerializableExceptionが発生します。 先に進む前に、少し説明しましょう。

JVMがラムダ式に遭遇すると、組み込みのASMを使用して内部クラスを構築します。 では、この内部クラスはどのように見えますか? コマンドラインでjdk.internal.lambda.dumpProxyClassesプロパティを指定することにより、この生成された内部クラスをダンプできます。

-Djdk.internal.lambda.dumpProxyClasses=<dump directory>

ここで注意してください:私たちが交換するときプロジェクトがサードパーティのライブラリに依存している場合、JVMは予期しない生成された内部クラスをかなり多くダンプする可能性があるため、ターゲットディレクトリでは、このターゲットディレクトリは空にすることをお勧めします。

ダンプした後、この生成された内部クラスを適切なJavaデコンパイラーで検査できます。

上の図では、生成された内部クラスは、ラムダ式のターゲットタイプであるRunnableインターフェイスのみを実装しています。 また、 run メソッドでは、コードは NotSerializableLambdaExpression.lambda $ getLambdaExpressionObject $ 0 メソッドを呼び出します。これは、Javaコンパイラーによって生成され、ラムダ式の実装を表します。

この生成された内部クラスはラムダ式の実際のクラスであり、 Serializable インターフェイスを実装していないため、ラムダ式はシリアル化には適していません。

2.2. ラムダをシリアル化する方法

この時点で、問題はポイントに落ちます:生成された内部クラスに Serializable インターフェースを追加する方法は? 答えは、ラムダ式を機能インターフェイスシリアライズ可能インターフェイスを組み合わせた交差型でキャストすることです。

たとえば、RunnableSerializableを交差型に組み合わせてみましょう。

Runnable r = (Runnable & Serializable) () -> System.out.println("please serialize this message");

ここで、上記の Runnable オブジェクトをシリアル化しようとすると、成功します。

ただし、これを頻繁に行うと、多くの定型文が導入される可能性があります。 コードをクリーンにするために、RunnableSerializableの両方を実装する新しいインターフェイスを定義できます。

interface SerializableRunnable extends Runnable, Serializable {
}

次に、それを使用できます。

SerializableRunnable obj = () -> System.out.println("please serialize this message");

ただし、シリアル化できない引数をキャプチャしないように注意する必要もあります。 たとえば、別のインターフェイスを定義しましょう。

interface SerializableConsumer<T> extends Consumer<T>, Serializable {
}

次に、実装として System.out ::printlnを選択できます。

SerializableConsumer<String> obj = System.out::println;

その結果、NotSerializableExceptionが発生します。 これは、この実装が引数として System.out 変数をキャプチャするためです。この変数のクラスは、シリアル化できないPrintStreamです。

3. 根底にあるメカニズム

この時点で、私たちは疑問に思うかもしれません:交差型を導入した後、下で何が起こりますか?

議論の基礎を築くために、別のコードを準備しましょう。

public class SerializableLambdaExpression {
    public static Object getLambdaExpressionObject() {
        Runnable r = (Runnable & Serializable) () -> System.out.println("please serialize this message");
        return r;
    }
}

3.1. コンパイルされたクラスファイル

コンパイル後、 javap を使用して、コンパイルされたクラスを検査できます。

javap -v -p SerializableLambdaExpression.class

-v オプションは詳細なメッセージを出力し、-pオプションはプライベートメソッドを表示します。

また、Javaコンパイラが $ deserializeLambda$メソッドを提供していることがわかります。このメソッドはSerializedLambdaパラメータを受け入れます。

読みやすくするために、上記のバイトコードをJavaコードに逆コンパイルしてみましょう。

上記の$deserializeLambda $ メソッドの主な責任は、オブジェクトを作成することです。 まず、ラムダ式の詳細のさまざまな部分を使用して、SerializedLambdagetXXXメソッドをチェックします。 次に、すべての条件が満たされると、 SerializableLambdaExpression :: lambda $ getLambdaExpressionObject $ 36ab28bd $1メソッド参照を呼び出してインスタンスを作成します。 それ以外の場合は、IllegalArgumentExceptionをスローします。

3.2. 生成された内部クラス

コンパイルされたクラスファイルを検査するだけでなく、新しく生成された内部クラスも検査する必要があります。 それでは、 jdk.internal.lambda.dumpProxyClasses プロパティを使用して、生成された内部クラスをダンプしましょう。

上記のコードでは、新しく生成された内部クラスは、RunnableインターフェイスとSerializableインターフェイスの両方を実装しているため、シリアル化に適しています。 また、追加のwriteReplaceメソッドも提供します。 内部を見ると、このメソッドはラムダ式の実装の詳細を説明するSerializedLambdaインスタンスを返します。

閉ループを形成するために、もう1つ欠けているものがあります。それは、シリアル化されたラムダファイルです。

3.3. シリアル化されたラムダファイル

シリアル化されたラムダファイルはバイナリ形式で保存されるため、16進ツールを使用してその内容を確認できます。

シリアル化されたストリームでは、16進数の「 AC ED 」(Base64では「rO0」)がストリームのマジックナンバーであり、16進数の「0005」がストリームバージョンです。 ただし、残りのデータは人間が読める形式ではありません。

Object Serialization Stream Protocol によると、残りのデータは次のように解釈できます。

上の図から、シリアル化されたラムダファイルに実際にSerializedLambdaクラスデータが含まれていることがわかります。 具体的には、10個のフィールドと対応する値が含まれています。 また、 SerializedLambdaクラスのこれらのフィールドと値は、コンパイルされたクラスファイルの$deserializeLambda$メソッドと生成された内部クラスのwriteReplaceメソッドの間のブリッジです。

3.4. すべてを一緒に入れて

次に、さまざまなパーツを組み合わせます。

ObjectOutputStream を使用してラムダ式をシリアル化すると、 ObjectOutputStream は、生成された内部クラスにSerializedLambdaを返すwriteReplaceメソッドが含まれていることを検出します。 ] 実例。 次に、 ObjectOutputStream は、元のオブジェクトではなく、このSerializedLambdaインスタンスをシリアル化します。

次に、 ObjectInputStream を使用してシリアル化されたラムダファイルを逆シリアル化すると、SerializedLambdaインスタンスが作成されます。 次に、 ObjectInputStream はこのインスタンスを使用して、SerializedLambdaクラスで定義されたreadResolveを呼び出します。 また、 readResolve メソッドは、キャプチャクラスで定義された $ deserializeLambda$メソッドを呼び出します。 最後に、逆シリアル化されたラムダ式を取得します。

要約すると、 SerializedLambdaクラスは、ラムダシリアル化プロセスの鍵です。

4. 結論

この記事では、最初に失敗したラムダシリアル化の例を見て、失敗した理由を説明しました。 次に、ラムダ式をシリアライズ可能にする方法を紹介しました。 最後に、ラムダシリアル化の根本的なメカニズムを調査しました。

いつものように、このチュートリアルのソースコードはGitHubにあります。