JDK1.8 序列化機制淺析

注意:本文語言環境為:max os x 10 ,jdk 1.8

前言

對于一個對象實例,如果我想在系統重啟后,仍能重現。那么這個時候通過數據庫持久化可以將對象實例存儲到數據庫內,在需要的時候再取出來,進行對象的復現。但是如果我想在進行遠程調用或者跨平臺進行數據傳輸時,對方接收到的對象實例和我傳輸的是一致的,那么這個時候如果實現呢?請看下面這段代碼:

public class Logon {
  Date date = new Date();
  String username;
  public Logon(String name, String pwd) {
    username = name;
    password = pwd;
  }
}

如果在進行數據傳輸時,不能保證對象的實例是同一個,對于Logon類而言,那么date屬性的值就不能保證是一致的。

如果我想保持對象實例的一致性,那么怎么做呢?實現序列化接口即可。

public class Logon implements Serializable {
  Date date = new Date();
  String username;
  public Logon(String name, String pwd) {
    username = name;
    password = pwd;
  }
}

1、標記接口

看到這,我們就有一個疑問:為嘛序列化接口是一個空接口,憑聲明一個空接口就可以進行對象實例的持久化?
我們來看jdk源碼內的序列化接口聲明:
public interface Serializable {
}

這種接口也稱為標記接口(類似的還有cloneable),只需要類實現該接口即可,jvm底層自動會幫你完成這個接口的特殊功能(在有的情況下)。

2、序列化接口實現原理

但是到這,我們上面的疑問其實還沒解答啊。

序列化接口作為一種將對象轉換為流數據的方法,必須通過ObjectOutputStream/ObjectInputStream對象進行流的傳輸。在進行流傳輸時,ObjectOutputStream/ObjectInputStream會調用其對應的writeObject/readObject方法進行對象的序列化操作(具體的代碼我就不貼了,太長了)。

這個時候就有一個問題, 我就不想用系統自帶的序列化方法,我想自己自定義,怎么辦?
好辦,在類內添加如下方法。

private void writeObject(java.io.ObjectOutputStream s)
        throws IOException;
private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException;

寫完這個代碼之后,心里虛啊,這里有幾個奇怪的地方。
1)既然是實現接口的功能,為嘛這個訪問權限是private
2)方法寫完了之后,在這個類內都沒有調用的地方。
這2個問題,其實我也不知道怎么解釋(坐等高人……)。其實,在看源碼之后(代碼在手,天下我有),可以給出如下的解釋(還是debug靠譜):
首先,在這個類內沒有地方調用,是因為這2個方法是在ObjectOutputStream/ObjectInputStream內調用的,是由這2個類內的writeObject/readObject調用對象內覆寫的writeObject/readObject方法進行對象的序列化操作。
至于private訪問權限,有萬能的反射。
來,我們看ObjectOutputStream的代碼:

private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            //判斷是否覆寫了writeObject方法
            if (slotDesc.hasWriteObjectMethod()) {
                PutFieldImpl oldPut = curPut;
                curPut = null;
                SerialCallbackContext oldContext = curContext;

                if (extendedDebugInfo) {
                    debugInfoStack.push(
                        "custom writeObject data (class \"" +
                        slotDesc.getName() + "\")");
                }
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    //反射調用覆寫的writeObject方法
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }

                curPut = oldPut;
            } else {
                //調用默認的writeObject方法
                defaultWriteFields(obj, slotDesc);
            }
        }
    }

3、自定義序列化的另一種方式

上面不是提到了自定義序列化方式嗎,除了覆寫2個方法外,還有一種方式,就是實現Externalizable接口。

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

4、transient關鍵字

上面介紹的都是自定義序列化方法,但是對于一個成員變量,如果我不想對其進行序列化操作,這個時候如果僅采用自定義序列化,那么肯定要完全重寫ObjectOutputStream/ObjectInputStream內的序列化/反序列化代碼,否則一旦調用defaultWriteObject/defaultReadObject,那么成員變量就自動進行了序列化的操作。
此時transient關鍵字就發揮作用了。這個關鍵字會告訴defaultWriteObject方法,這個成員變量不用進行序列化了。如果一個成員變量被transient修飾符修飾了,但是又想進行序列化怎么辦?就重寫該類的writeObject /readObject方法吧,在這2個方法內自定義序列化與反序列化。

5、序列化與反序列化

對于上面的Logon類,生成一個對象實例,并序列化后,再新增一個屬性password(可以將序列化完成的結果持久化到硬盤),變成如下類:

public class Logon implements Serializable {
    private Date date = new Date();
    private String username;
    private String password;
}

那么再進行反序列化調用readObject時,會發生什么呢?
會拋出ClassNotFoundException。為什么會這樣呢?因為序列化實際上會自動生成一個版本信息,類似于這樣:

public class Logon implements Serializable {
  private static final long serialVersionUID = -5664935080424674771L;
  private Date date = new Date();
  private String username;
  private String password;
}

那如果沒生成serialVersionUID屬性會怎樣?那么此時序列化機制會根據對象內的成員屬性自動生成一個序列化版本號(猜測類似于hashcode),當對象的成員變量有變更時,那么serialVersionUID就會不一致,反序列化時就會認為這2個不是一個類,拋ClassNotFoundException也在情理之中了。

如何避免這種情況呢?正常來說,最好在實現序列化接口時,手動生成一個版本信息。那么即使對象的成員變量有變更,在進行反序列化時也會將未變更的成員變量進行自動反序列化。

還有一種情況,就是父類實現了序列化接口,但是子類沒有。那么如果父類有變更,子類的序列化版本會發生變化嗎?不會!(原因,我也不知道,這么亂,我也是醉了)

如果系統是使用maven進行版本管理,如果成員變更,但是序列化版本沒變,那么此時可以考慮進行版本升級,避免不同版本之間進行錯誤的序列化操作。

6、其他情況

對于static修飾的成員變量,也是不會進行序列化的。因為所謂的序列化,是對對象的實例而言,而一個類的static成員變量,是一個類共享的,不屬于任何一個對象實例,因而不會將static成員變量進行序列化。如果想序列化static成員變量,那么就需要自定義序列化與反序列化。

7、實例

上面講了這么多,我們來段代碼分析分析,加深一下印象。還是對hashmap源碼進行分析(為嘛我就跟hashmap過不去呢,哈哈)。

先對writeObject進行分析:

private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
        //獲取當前實例的位桶大小
        int buckets = capacity();
        // Write out the threshold, loadfactor, and any hidden stuff
        //調用ObjectOutputStream的默認序列化方法
        s.defaultWriteObject();
        //序列化位桶
        s.writeInt(buckets);
        //序列化當前對象的大小
        s.writeInt(size);
        //序列化每個entry內的數據,即每個位桶內的K-V
        internalWriteEntries(s);
}

其中:

//瞬態的size
transient int size;
//獲取當前實例的位桶容量
final int capacity() {
        return (table != null) ? table.length :
            (threshold > 0) ? threshold :
            DEFAULT_INITIAL_CAPACITY;
}
//遍歷所有位桶,逐個序列化位桶內的K-V
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
        Node<K,V>[] tab;
        if (size > 0 && (tab = table) != null) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    s.writeObject(e.key);
                    s.writeObject(e.value);
                }
            }
        }
}

從上面代碼可以看出,hashmap將位桶的數據用transient進行修飾:

transient Node<K,V>[] table;

進行這樣修飾之后,也不是說在進行序列化時忽略掉,而是基于一種優化思想:在進行序列化時,如果該位桶有數據,我就進行序列化,如果沒有數據,那么我就不進行序列化。

再對readObject進行分析:

private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        //調用ObjectInputStream內的默認方法進行反序列化
        s.defaultReadObject();
        //類似于進行反構造
        reinitialize();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " + loadFactor);
        //對于這個丟棄處理表示很不解
        s.readInt();
        //讀取需進行反序列化的實例大小            
        int mappings = s.readInt(); 
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " + mappings);
        else if (mappings > 0) { 
            // range of 0.25...4.0
            //隨機生成一個類似于負載因子的數
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float)mappings / lf + 1.0f;
            //生成位桶大小
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            //生成閾值
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;

            //遍歷,逐個反序列化
            //從這也可以看出,在進行反序列化的時候,不保證每個位桶內的數據順序與原來的保持一致
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }

8、小結

隨著技術的發展,如今的序列化已經不限于java原生的序列化機制了.如果想了解javaweb中的json序列化,可以看這個文章了解一下,不過目前的使用已經遠超這篇文章的敘述了:
http://www.ibm.com/developerworks/cn/web/wa-lo-json/?ca=drs-tp3308

注:部分代碼與思想來自于Thinking in java(fourth edition)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容