デフォルトのシリアルバージョンUIDを作成するコードを読んでみる
ダイチャンさんが、ブログでJavaのシリアルバージョンUIDの話をされていて、最後の一文が自分も気になったので調べました。
ランタイムが未定義のserialVersionUIDにどんな値を割り当ててるのか、見る方法は分からなかった。誰か知ってますか?
SerializableとserialVersionUID - 都元ダイスケ IT-PRESS
元記事のコメントにnobeansさんが書いてくださったとおり、読むべきコードはこちら。
- 実装
- Javadoc
ドキュメントに計算の内容まで相当詳細に書かれていますが、勉強ということで自分用にまとめます。
シリアルバージョンコマンド(serialver)
serialVersionUIDの値は、開発システムによって提供されます。多くの開発システムでは、その値はserialverと呼ばれるコマンドの出力結果です。
『プログラミング言語Java 第4版』,p.490,ピアソン・エデュケーション,2007.4.
とのことで、JDKにもserialverというツールが付属しています。
- 実装
- Javadoc
このツールが、ObjectStreamClassのgetSerialVersionUID()を呼び、getSerialVersionUID()が、プライベートstaticなメソッドであるcomputeDefaultSUID(Class cl)を呼び出しています。
概要
ObjectStreamClass#computeDefaultSUIDがやっていることをざっくり説明すると、ひたすらクラスやメンバーの修飾子を確認して、条件に合ったら値を書きだし、最後にSHA-1でハッシュ値を得ています。
この時、クラスやメンバーの修飾子は、次の方法で確認します。
- 調べたいデータ(クラス、フィールド、メソッド、etc)の整数表現を得る
- Modifierのフィールドを利用して、調べたいパターンを作る
- 1と2を掛け合わせる
Modifierとは、java.lang.reflectパッケージにあるModifierクラスのことです。Modifierクラスは、クラス修飾子やアクセス修飾子の定数フィールドを持っています。フィールドの値は、ビット列です。
どんなビット列を持っているかは、ObjectStreamClassクラスのコードをマネして、確認できます。
public class Main { protected void compute(Class cl) { System.out.println(cl.getName() + ": "); // パラメータのビット表現をゲト int clazz = cl.getModifiers(); System.out.printf("%16sB\t:getModifiers()\n", Integer.toString(clazz, 2)); // Modifierが持つ定数フィールドの値を確認 System.out.printf("%16sB\t:%s\n", Integer.toString(Modifier.PUBLIC, 2), Modifier.toString(Modifier.PUBLIC)); System.out.printf("%16sB\t:%s\n", Integer.toString(Modifier.FINAL, 2), Modifier.toString(Modifier.FINAL)); System.out.printf("%16sB\t:%s\n", Integer.toString(Modifier.INTERFACE, 2), Modifier.toString(Modifier.INTERFACE)); System.out.printf("%16sB\t:%s\n", Integer.toString(Modifier.ABSTRACT, 2), Modifier.toString(Modifier.ABSTRACT)); // 4つのクラス修飾子、アクセス修飾子を組み合わせた判定パターンを作成 int accesscint = Modifier.PUBLIC | Modifier.FINAL | Modifier.INTERFACE | Modifier.ABSTRACT; System.out.printf("%16sB\t:public-final-interface-abstract\n", Integer.toString(accesscint, 2)); // パラメータのクラス情報を判定した値をゲト int classMods = cl.getModifiers() & (Modifier.PUBLIC | Modifier.FINAL | Modifier.INTERFACE | Modifier.ABSTRACT); System.out .printf("%16sB\t:classMods\n", Integer.toString(classMods, 2)); } public static void main(String[] args) { Main hoge = new Main(); hoge.compute(Hoge.class); } } // 調査対象のクラス(ここではインタフェース) interface Hoge { // メソッドなし }
結果。
Hoge:
11000000000B :getModifiers()
1B :public
10000B :final
1000000000B :interface
10000000000B :abstract
11000010001B :public-final-interface-abstract
11000000000B :classMods
上の例では、public、final、interface、abstractのビットパターンを作成して、Class#getModifiers()で取得した表現と比較しています。
serialver算出の流れ
さて、以下は、コードの内容を自然文で書くというナンセンスなメモですが・・・(マァイッカ・・・)Javadocに沿って確認します。
DataOutputStreamに値を書き出していき、最後にハッシュ化します。
ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream dout = new DataOutputStream(bout);
ストリームへの書き出しに使われるエンコードは、修正UTF-8といって、通常のUTF-8とは異なるそうです。
0. 事前判定
最初に、これからserialverを算出する対象のクラスが、
- Serializableインタフェース自身でない
- Proxyクラスである
かどうかを調べます。上2つのどちらかに該当する場合は、すぐに0Lを返して終了します。
なぜそうする必要があるのかは、よく分かりません。
プロキシクラスには、直列化可能なフィールドおよび 0L の serialVersionUID がありません。つまり、プロキシクラスの Class オブジェクトが java.io.ObjectStreamClass の static lookup メソッドに渡されたときに、返される ObjectStreamClass インスタンスには次の特性があります。
- getSerialVersionUID メソッドを呼び出すと、0L が返される
(略)
Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle
仕様なのは分かるけれども、ダイナミックプロキシって何に使われてるん?っていう・・・無知ですみません。すみません。(また宿題?)
とにかく、上記のクラス以外の場合だけ、次の処理に進みます。
1. クラス名
クラス名を取得して、ストリームに書き出します。
2. クラス修飾子
public、final、interface、abstractのビットパターンを生成して、対象クラスのパターンを確認します(上のコードは、ちょうどこの部分を参考にしました)。
ここで、ちょっとバグ対応らしき処理が入ります。
まず、定義されているメソッドの数をClass#getDeclaredMethods()で取得します。次に、対象クラスがinterfaceのとき、メソッド数が0ならabstractのビットを除去し、メソッド数が1以上ならabstractのビットを追加します。
たとえば、上のコードの実行結果では、左端にabstractのビットが立っています。しかし、対象クラス(インタフェース)には、メソッドが存在しません。そのため、この処理を通った後には、次のように左端のabstractビットが取り去られます。
11000000000B :classMods
宣言メソッド数:0
1000000000B :classMods インタフェースのメソッド数チェック済
ここまでの値を、一旦ストリームに書き出します。
3.インタフェース名
対象のクラスが配列クラスでない場合のみ、ここでの処理を行います。これは、昔(1.2)、インタフェースを取得するメソッドの挙動が違ったらしく、そのための処置みたい・・・でもタブン枝葉なので気にしなくてOK。(2011/06/04 追記: メモ: JDK1.1とJDK1.2のArrayクラスの違い - 虎塚)
Class#getInterfaces()で、クラスが直接実装しているインタフェース群を取得します。取得したすべてのインタフェース名を名前の順でソートして、ストリームに書き出します。
4. フィールド名
Class#getDeclaredFields()で、クラスに定義されたフィールドを取得します。フィールドを名前の順にソートして、配列に格納します。
さらに、publiv、private、protected、static、final、volatile、transientのビットパターンを生成します。
対象のフィールドが、
- privateフィールドである
- static transientフィールドである
のどちらかあれば、ストリームに
を書き出します。
5. クラス初期化子
もしクラス初期化子が存在すれば、
を書き出します。
ところで、クラス初期化子の有無を確認するために、ネイティブのコードが呼ばれています。リフレクションを使えばフィールドやメソッドの情報は取れますが、static初期化のコードは取れません。だからネイティブのコードで確認する必要がある・・・のでしょうか?
上のコードのJava_java_io_ObjectStreamClass_hasStaticInitializerというメソッドに、実装があります。
6. コンストラクタ
Class#getDeclaredFields()で、クラスに定義されたフィールドを取得します。フィールドをシグネチャの順にソートして、配列に格納します。
サテハテ、閑話休題。
インタフェースやフィールドへの名前の順ソートと同じく、ここでもComparatorが骨格実装されているのですが、シグネチャ順のソートってどんな結果になるのでしょう?
分からなかったので確認してみます。
public class Main { protected void compute(Class cl) { Constructor[] cons = cl.getDeclaredConstructors(); MemberSignature[] consSigs = new MemberSignature[cons.length]; for (int i = 0; i < cons.length; i++) { consSigs[i] = new MemberSignature(cons[i]); } Arrays.sort(consSigs, new Comparator() { public int compare(Object o1, Object o2) { String sig1 = ((MemberSignature) o1).signature; String sig2 = ((MemberSignature) o2).signature; return sig1.compareTo(sig2); } }); // ソート結果を表示して確認 for (int i = 0; i < consSigs.length; i++) { MemberSignature sig = consSigs[i]; System.out.println("[" + (i + 1) + "] " + sig.signature.toString()); } } public static void main(String[] args) { Main hoge = new Main(); hoge.compute(Hoge.class); } // 内部クラスMemberSignatureの実装とか色々省略。。。 } // 調査対象のクラス(※上のコードでインタフェースだったのをクラスに変更) class Hoge { // テキトーにコンストラクタを追加 Hoge() { } Hoge(Object obj) { } Hoge(int i, int j) { } Hoge(String str) { } Hoge(long l, int i) { } }
結果。
[1] ()V [2] (II)V [3] (JI)V [4] (Ljava/lang/Object;)V [5] (Ljava/lang/String;)V
JVMタイプシグネチャのアルファベット順にいくのか。そーか。という感じです。
・・・閑話終了。
ここでは、public、private、protected、static、final、synchronized、native、abstract、strictのビットパターンを生成します。
そして、private以外のコンストラクタについて、ストリームに、
を書き出します。
7. メソッド
2.で取得しておいたメソッド群を使います。メソッドを名前の順でソートした後、さらにシグネチャの順でソートしています。
上のコンストラクタと同じく、public、private、protected、static、final、synchronized、native、abstract、strictのビットパターンを生成します。
ここでも、private以外のメソッドについて、ストリームに、
を書き出します。
8-9. ハッシュ値
最後に、やっとハッシュ化です。あとちょっと!
MessageDigesを使っています。"SHA"が渡されているので、SHA-1アルゴリズムで固定長ハッシュ値を得ています。
- (参考)http://java.sun.com/javase/ja/6/docs/ja/technotes/guides/security/StandardNames.html#MessageDigest
最終的な値を作成する方法は、知らなかったので勉強になりました。…説明しづらいので引用すると、
MessageDigest md = MessageDigest.getInstance("SHA"); byte[] hashBytes = md.digest(bout.toByteArray()); long hash = 0; for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) { hash = (hash << 8) | (hashBytes[i] & 0xFF); }
ハッシュ値は、byteデータの配列として取得します。「配列からbyteデータを1個ずつ取り出して0xFFでマスクした値」と、「最終的な値(初期値0)を8ビットずつ左シフトした値」のORを取ることで、ハッシュ値を作成しています。
というわけで、デフォルトのシリアルバージョンUIDをめでたくゲットできました。お疲れさまでした。
『プログラミング言語Java』の記述
元記事のコメントでmegascusさんが言及してくださっているので、『プログラミング言語Java 第4版』の記述も確認しておきます。
オブジェクトが書き込まれる時には、シリアルバージョンUID(serial version UID)(一意な識別子)である64ビットlong値が一緒に書き込まれます。デフォルトでは、この識別子は、完全なクラス名、スーパーインタフェースおよびメンバーの安全なハッシュ値です。
『プログラミング言語Java 第4版』,p.489,ピアソン・エデュケーション,2007.4.
イィィィグザクトリィ(そのとおりでございます)
余談
このコードで使われているDataOutputStream。flush後に、closeされていません。。。どういうことなの・・・。
特に悲しい話題ではありませんが、ネコを挟みつつお送りしました。