再考:serialVersionUID
こんにちは。
今回は、Java でシリアライズを扱うときに登場する serialVersionUID について深堀りしてみます。
シリアライズについては公式にドキュメント⁽¹⁾もありますが、詳細まで追いかけている方は意外と少ないかもしれません。
そこで、本記事では Java 21 を前提としつつ、 serialVersionUID を中心にシリアライズの仕組みや注意点について記載してみます。
Java におけるシリアライズの基本
Java では以下のようなコードで値やオブジェクトをシリアライズすることができます。
このように、オブジェクトをバイト列に直列化( シリアライズ )して、再度オブジェクトに戻す( デシリアライズ )ことができます。
これによって、ファイルやネットワークを通じたデータのやり取りに使うことができます。
serialVersionUID とは
クラスをシリアライズ可能にするためには java.io.Serializable を実装します。
この時に static final long serialVersionUID で宣言した値が クラスのバージョン になります。
もし互換性のないクラスで古いバージョンのバイトストリームを読み込んだ場合、serialVersionUID が一致しないことによって デシリアライズに失敗させる ことができます。
例えば、以下のようにクラスの中で serialVersionUID を宣言します。
ここで宣言した 100L が、シリアライズ時に書き込まれる UID として使われ、デシリアライズ時にバージョンチェックされます。
なお、JDK の内部実装では「static final かつ serialVersionUID という名前のフィールド」を探しているため、int や byte など大きさの違う型でも見つけてしまいます⁽²⁾。
(ただし、仕様としては 64bit の long を使うことになっています。)
ここで宣言した serialVersionUID がバイトストリームに書き込まれ、デシリアライズ時にチェックが行われます。
ストリームのバージョン管理の責任はデシリアライズするクラスにあります。
クラスに互換性の無い変更を加えた場合に serialVersionUID を変更することで、デシリアライズを正しく失敗させることができます。
暗黙の serialVersionUID のその計算方法
ところで、実はこの serialVersionUID は明示的に宣言しなくてもシリアライズ/デシリアライズ時に自動で計算されます。
具体的には、クラスのシグネチャ(クラス名、フィールドやメソッドなどの情報)から生成したバイト列を SHA-1 でハッシュ化したものが使われます⁽³⁾。
そのため、クラスに互換性の無くなるような変更を加えた際には暗黙の serialVersionUID が変わり、不正なデータを無理に読み込むことなくデシリアライズを失敗させることができます。
仕様通りに手動で計算してみたものが以下のコードになります。
クラスが変わったときにバージョンを変えたいだけであればこの暗黙の UID 計算を利用すれば良さそうに見えますが、クラスが変わらなくてもコンパイラや Java バージョンなどが変わると自動生成される UID が変わる可能性があるため、明示的に宣言することを推奨されています。
また、ストリームの互換性を保ちながらクラスを変更するなど、柔軟な処理をするためには明示的に宣言しておく必要があります。
UID を自分でコントロールすることで、「どの段階の変更で互換性を破棄するか」「どこまで互換を保つか」を明確にできます。
まとめ
serialVersionUID はクラスのバージョン管理に利用され、クラスを互換性のあるかたちで拡張したり、互換性を断つために意図的にエラーを起こしたりするのに役立ちます。
明示的に宣言しなくても暗黙に計算されますが、クラスが変わらなくてもコンパイラや Java バージョンによって計算結果が変わり得るので注意が必要です。