技術とか戦略とか

IT技術者が技術や戦略について書くブログです。

java:シリアルバージョンUIDの用途を調べてみた

javaのシリアルバージョンUIDの用途を理解したのでメモ。
 
前提として、javaではインスタンスをバイナリファイルとして保存することができます。
しかし、インスタンスのクラスの設計を変更した場合、前の設計のバイナリファイルを読み込むと、現在の設計のクラスとの整合性が取れずに処理結果が不正になる恐れがあります。
 
その際に、クラスにシリアルバージョンUIDを定義しておくことで、前の設計のバイナリファイルを読み込んだ際に例外を吐き出すことができるようになります。
シリアルバージョンUIDはバイナリファイルに保存されるのですが、現在のクラスのシリアルバージョンUIDとバイナリファイルのシリアルバージョンUIDが異なる場合に例外として判定されます。
そのため、クラスの設計を変更する度にシリアルバージョンUIDを変更していれば、前の設計のバイナリファイルを読み込んだ際に例外となります。
 
以下、実際に挙動を確認した結果です。
 
【テストコード】
■Item.java
package seriaTest;

import java.io.Serializable;

public class Item implements Serializable{

    private static final long serialVersionUID = 81923983183821L; //①
//    private static final long serialVersionUID = 81923983183822L; //②

    private String itemName;
    private int price;
//    private int stock; //②

    public Item(String itemName, int price) { //①
//    public Item(String itemName,  int price, int stock) { //②
        this.setItemName(itemName);
        this.setPrice(price);
//        this.setStock(stock);  //②
    }

    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
/*//②
    public long getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }
*///②
}
 
■MainInput.java
package seriaTest;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class MainInput {
    public static void main(String args) throws Exception {
        Item item1 = new Item("商品1",100); //①
//        Item item1 = new Item("商品1",100,10000); //②

        // インスタンスを保存
        FileOutputStream fos = new FileOutputStream("C:\\tmp\\instnce.dat");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(item1);
        oos.flush();
        oos.close();

    }
}
 
■MainOutput.java
package seriaTest;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class MainOutput {
    public static void main(String args) throws Exception {

        // インスタンスを読み込み
        FileInputStream fis = new FileInputStream("C:\\tmp\\instnce.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Item item2 = (Item)ois.readObject();
        ois.close();

        System.out.println(item2.getItemName());
        System.out.println(item2.getPrice());
//        System.out.println(item2.getStock()); //②
    }
}
 
【実行結果】
■当初のクラス設計(①を有効、②をコメントアウト)
・MainInputを実行
インスタンスがファイルとして保存される。
 
C:\tmp>dir
 ドライブ C のボリューム ラベルは XXX です
 ボリューム シリアル番号は XXX です

 C:\tmp のディレクトリ

2018/12/01  10:18    <DIR>          .
2018/12/01  10:18    <DIR>          ..
2018/12/01  10:36                91 instnce.dat
               1 個のファイル                  91 バイト
               2 個のディレクトリ  24,059,305,984 バイトの空き領域

C:\tmp>
 
・MainOutputを実行
インスタンスを読み込めていることを確認できる。
 
商品1
100
 
■クラス設計変更(①をコメントアウト、②を有効)
・MainOutputを実行(変更前のクラス設計時のファイルを読み込んでみる)
インスタンス内に保存されたシリアルバージョンUIDが現在のクラスのシリアルバージョンUIDと異なるため、現在のクラスに対応したインスタンスではないと判断して異常終了する。
(シリアルバージョンUIDを変更しないと、現在のクラスに対応したインスタンスでなくても読み込み時にエラーにならずに後続に進んでしまう。不正なインスタンスを用いることで後続処理の処理結果が不正になる可能性が出てしまう。)
 
Exception in thread "main" java.io.InvalidClassException: seriaTest.Item; local class incompatible: stream classdesc serialVersionUID = 81923983183821, local class serialVersionUID = 81923983183822
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)
    at seriaTest.MainOutput.main(MainOutput.java:12)
 
・MainInputを実行→MainOutputを実行
新しいクラスでインスタンスを作り直せば正常に処理できる。
 
商品1
100
10000