SerializableとserialVersionUID

以前、Javaシリアライズ仕様がよくわからなくてエントリを書いた。

難解なSerializableという仕様について俺が知っていること、というか俺の理解 - 都元ダイスケ IT-PRESS

まぁ、わからないまま書いたので論点もあっちゃこっちゃ飛びながらのエントリだったわけだけど、一年経ってコメントを頂いた。

serialVersionUIDに関しては、定義をしない場合にはクラスの構造を解釈して勝手にコンパイラが生成してくれます。
つまり、クラスのフィールドを変更したら勝手に変更してくれます。
問題点としては、コンパイラが勝手に計算してくれるので、複数のコンパイラをまたぐ場合に同じ定義のはずだけれども失敗することがある(らしい)ということです。
つまり、記事に書いてあることとは逆ですね。
JavaDocに記載されているのでご参照ください。
http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/java/io/Serializable.html#そもそもSun実装以外のコンパイラを使うことがあるのかという疑問はありますが。
#そして、自分は複数のコンパイラ(+VM)で失敗することを見たことがないというか、Sun実装以外のコンパイラを使用したことがない。

難解なSerializableという仕様について俺が知っていること、というか俺の理解 - 都元ダイスケ IT-PRESS

なるほどっ。ありがとうございます。デフォルトコンストラクタを定義しないとコンパイラが勝手に作ってくれるアレだね! 非staticなメンバークラスを定義した時にコンパイラが勝手にフィールドを追加したりするアレだね! と思ったので実験してみた。

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class Main implements Serializable {
 
  public static void main(String[] args) throws Exception {
    // (1)
    Constructor<Main> mainConst = Main.class.getDeclaredConstructor();
    System.out.println(mainConst);
   
    // (2-1)
    Main main = new Main();
    Child child = main.new Child(128);
    Constructor<Child> childConst = null;
    try {
      childConst = Child.class.getDeclaredConstructor(int.class);
      System.out.println("NG");
    } catch (NoSuchMethodException e) {
      System.out.println("OK");
    }
    childConst = Child.class.getDeclaredConstructor(Main.class, int.class);
    System.out.println(childConst);

    // (2-2)
    Field f = Child.class.getDeclaredField("this$0");
    Object object = f.get(child);
    System.out.println(object == main);
   
    // (3)
    Field field = Main.class.getDeclaredField("serialVersionUID");
    Object id = field.get(null);
    System.out.println(id);
  }
 

  class Child {
   
    private final int hoge;

    Child(int hoge) {
      this.hoge = hoge;
    }
  }
}

コンストラクタを1つも宣言しないと…

(1)は、Mainクラスには引数なしのコンストラクタを明示的に宣言していないけど、リフレクションでは取れる、という例。実行結果として「public Main()」みたいな文字列が表示されるので、コンストラクタは確かに存在することが分かる。もし指定した(引数なしの)コンストラクタが存在しないのならばgetDeclaredConstructorの時点でNoSuchMethodException飛ぶのがJavaの仕様である。

非staticの内部クラスを宣言すると…

(2)に先立って、まず用語整理。Mainを外包クラス(enclosing class)、Childを内部クラス(inner class)とします。それぞれのインスタンスを enclosing instance / inner instance と呼ぶ事にします。

で、(2-1)は、非static内部クラスのコンストラクタの第一引数に外包クラスが追加されている例だ。知らない人は、なぜコンパイラはこんな手を加えるのか、っての考えると面白いよ。本題からズレるのでその話は割愛。ポイントは、ChildのコンストラクタはChild(int hoge)と宣言したので Child.class.getDeclaredConstructor(int.class); でいいはずなのに、なぜか取れない。コンパイルしたバイトコードは実際は以下のような形をしているのだ。

public class Main implements Serializable {
  // 略
  class Child {
    final Main this$0;    
    Child(Main x, int hoge) {
      this$0 = x;
      this.hoge = hoge;
    }
  }
}

従って、Child.class.getDeclaredConstructor(Main.class, int.class); が正解。引き続き実行すると「Main$Child(Main,int)」と表示される。Childのコンストラクタ引数の数は、実は2個なんですね。

(2-2)は、上記コードの謎の this$0 フィールドの存在と同一性を確認しています。表示は「true」。

Serializableを実装して、serialVersionUIDを宣言しないと…!?

さて、本題は(3)です。1〜2でノリノリになってしまった。MainはSerializableを実装しているので、同じノリで自動生成してくれるんだろうなぁ、ということで Main.class.getDeclaredField("serialVersionUID"); してみた。が、

java.lang.NoSuchFieldException: serialVersionUID

くっ…。無いらしい。もっと単純にしてjadってみる。

import java.io.Serializable;
import java.lang.reflect.Field;

public class Main implements Serializable {
 
  public static void main(String[] args) throws Exception {
    Field field = Main.class.getDeclaredField("serialVersionUID");
    Object id = field.get(null);
    System.out.println(id);
  }
}
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name:   Main.java

import java.io.PrintStream;
import java.io.Serializable;
import java.lang.reflect.Field;

public class Main
    implements Serializable
{

    public Main()
    {
    }

    public static void main(String args[])
        throws Exception
    {
        Field field = Main.getDeclaredField("serialVersionUID");
        Object obj = field.get(null);
        System.out.println(obj);
    }
}

やっぱりねぇよー。あらためて仕様を読み直す。

直列化可能クラスが明示的に serialVersionUID を宣言しない場合、直列化ランタイムは、『Java(TM) Object Serialization Specification』に記述されているように、さまざまな局面に基づいて、そのクラスのデフォルトの serialVersionUID 値を計算します。

直列化ランタイムは
直列化ランタイムは

直列化ランタイムは


うおぉ。読み漏らした。自動生成するのはコンパイラじゃなくてランタイムだっ。というわけで、コンパイラ(javacコマンド)がバイトコードに埋め込むんじゃなくて、ランタイム(javaコマンド)が頑張るんだね。

というわけで、コンパイラをSun-JDKに固定したとしても、ランタイムが異なるとアレな結果になるかもしれないっぽい。

最後に。ランタイムが未定義のserialVersionUIDにどんな値を割り当ててるのか、見る方法は分からなかった。誰か知ってますか?