淺談Java的序列化機制

概念

一個對象如果想在硬盤上存儲,一定就需要藉助於一定的數據格式。這種把對象轉換爲硬盤存儲的格式的過程就叫做對象的序列化,同樣地,將這些文件再反向轉換爲程序中對象的操作就叫做反序列化

一些複雜的解決方案可能是將對象轉換爲json字符串的方式,這種方式的優點是易讀,但是效率還是太低,所以Java的序列化的解決方案是將對象轉換爲一個二進制流的形式,來實現數據的持久化,本篇文章將會來詳細講解序列化的實現和原理

實現

準備

我們這裏有一個普通的對象,要注意的是這個類和其中用到的所有對象都需要實現序列化接口Serializable:

class Demo implements Serializable {

    int val = 10;

    String time = new SimpleDateFormat("HH:mm:ss").format(new Date());

    A a = new A(20);

    @Override
    public String toString() {
        return "[hashcode=" + hashCode() + " val=" + val + ", time=" + time 
                + ", A.val=" + a.val +"]";
    }
}

這個A是一個普通的對象,如下:

class A implements Serializable {
    int val = 20;
    
    public A(int val) {
        this.val = val;
    }
}

現在我們有一個Demo對象,來輸出一下這個對象的標誌字符串:

        Demo demo = new Demo();
        System.out.println(demo.toString());

輸出結果:

[hashcode=1625635731 val=10, time=20:28:56, A.val=20]
序列化

現在,我們需要將這個對象序列化爲二進制流,則需要以下的操作:

        FileOutputStream fileOutputStream = new FileOutputStream("target");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

        objectOutputStream.writeObject(demo);
        objectOutputStream.flush();
        objectOutputStream.close();

這樣,demo對象就被我們持久化到硬盤的target文件中了

反序列化

反之,如果我們想將這個對象從target文件中取出,就需要如下的操作:

        FileInputStream fileInputStream = new FileInputStream("target");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

        Demo newDemo = (Demo)objectInputStream.readObject();
檢驗

現在,我們用以下的語句來檢驗這兩個對象是否是一個對象:

        System.out.println(newDemo.toString());
        System.out.println("demo == newDemo : " + (demo == newDemo));

輸出

[hashcode=885284298 val=10, time=20:28:56, A.val=20]
demo == newDemo : false

我們會發現,反序列化得到的對象雖然值和原有對象一致,但是其不是同一個對象,這一點很重要

原理

我們打開序列化生成的target文件,這裏需要用二進制流的方式打開:
文件
這裏可以將文件分爲5個部分:

  • 文件頭:聲明文件是一個對象序列化文件,同時聲明瞭序列化版本
  • 類描述:聲明類信息,包括類名、序列化id,以及域的個數等屬性
  • 屬性描述
  • 父類信息描述
  • 對象屬性的實際值

也就是說,在這個二進制文件中,通過這幾部分就能表明一個類的全部信息,在反序列化的過程中,Java將會按照指定的文件格式來從文件中恢復數據

注意事項

  • 序列化的類一定要實現Serializable接口
  • 序列化類中包含的自定義對象都需要實現Serializable接口

這兩點是爲什麼呢,我們來看ObjectOutputStream中的writeObject0方法,這裏截取了一小段:

            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }

這段代碼中的obj不僅僅是被序列化的對象,還會是這個對象中的所有字段,也就是說其中的域對象,必須是字符串、數組、枚舉和序列化接口中的一種,否則就會拋出異常

序列化ID

其實,還有一點注意事項,我留在了這裏來講:

  • 在序列化和反序列化之間,對象的字段名稱、類型和數量均不能改變

這是爲什麼呢,我們來看反序列化中的一塊代碼:

            if (model.serializable == osc.serializable &&
                    !cl.isArray() &&
                    suid != osc.getSerialVersionUID()) {
                throw new InvalidClassException(osc.name,
                        "local class incompatible: " +
                                "stream classdesc serialVersionUID = " + suid +
                                ", local class serialVersionUID = " +
                                osc.getSerialVersionUID());
            }

這是ObjectStreamClass中的initNonProxy方法中的一段,這個方法也就是讀取我們序列化文件的核心方法,用於初始化類描述符

不過我們重點不在這裏,重點是一個suid和osc.getSerialVersionUID()的比較,這時候就要涉及到一個序列化id的概念了,序列化id的聲明類似下面這種形式:

class Demo implements Serializable {

	// 這個序列化id一般的ide都會提供有自動生成的插件,感興趣的可以自行下載
	private static final long serialVersionUID = -5809782578272943999L;
	// ...
}

Java的反序列化成功與否的關鍵,就是比較文件的序列化id和類的序列化id是否一致,如果一致,則認爲文件中的對象和類對象是同一個對象,否則,就說明兩個類壓根就不是一個類,如果強行轉換則很有可能發生異常

但是我們之前沒有手動設置序列化id也一樣能反序列化成功不是嗎?其實,之前能反序列化成功僅僅是因爲我們沒有改動原來的類,如果我們沒有設置序列化id,則以下任何的操作,均會導致反序列化失敗:

  • 修改了字段/方法的名稱/類型
  • 添加或刪除字段/方法

看到了嗎,即使我們僅僅修改了字段的名稱,也會導致反序列化的失敗,如果不注意這一點,將會導致所有反序列化操作的崩潰,但是隻要我們設置一個序列化id,即使我們把類中元素刪的一乾二淨,也一樣會反序列化成功,只不過是丟失屬性而已

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章