JDK1.8 序列化機制淺析

注意:本文語言環(huán)境為:max os x 10 ,jdk 1.8

前言

對于一個對象實例囤热,如果我想在系統(tǒng)重啟后锹雏,仍能重現(xiàn)磷籍。那么這個時候通過數(shù)據(jù)庫持久化可以將對象實例存儲到數(shù)據(jù)庫內(nèi)即横,在需要的時候再取出來袄秩,進行對象的復(fù)現(xiàn)阵翎。但是如果我想在進行遠程調(diào)用或者跨平臺進行數(shù)據(jù)傳輸時,對方接收到的對象實例和我傳輸?shù)氖且恢碌闹纾敲催@個時候如果實現(xiàn)呢郭卫?請看下面這段代碼:

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

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

如果我想保持對象實例的一致性,那么怎么做呢词疼?實現(xiàn)序列化接口即可俯树。

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

1、標記接口

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

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

2秽晚、序列化接口實現(xiàn)原理

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

序列化接口作為一種將對象轉(zhuǎn)換為流數(shù)據(jù)的方法赴蝇,必須通過ObjectOutputStream/ObjectInputStream對象進行流的傳輸菩浙。在進行流傳輸時,ObjectOutputStream/ObjectInputStream會調(diào)用其對應(yīng)的writeObject/readObject方法進行對象的序列化操作(具體的代碼我就不貼了扯再,太長了)芍耘。

這個時候就有一個問題, 我就不想用系統(tǒng)自帶的序列化方法熄阻,我想自己自定義斋竞,怎么辦?
好辦秃殉,在類內(nèi)添加如下方法坝初。

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

寫完這個代碼之后,心里虛啊钾军,這里有幾個奇怪的地方鳄袍。
1)既然是實現(xiàn)接口的功能,為嘛這個訪問權(quán)限是private
2)方法寫完了之后吏恭,在這個類內(nèi)都沒有調(diào)用的地方拗小。
這2個問題,其實我也不知道怎么解釋(坐等高人……)樱哼。其實哀九,在看源碼之后(代碼在手,天下我有)搅幅,可以給出如下的解釋(還是debug靠譜):
首先阅束,在這個類內(nèi)沒有地方調(diào)用,是因為這2個方法是在ObjectOutputStream/ObjectInputStream內(nèi)調(diào)用的茄唐,是由這2個類內(nèi)的writeObject/readObject調(diào)用對象內(nèi)覆寫的writeObject/readObject方法進行對象的序列化操作息裸。
至于private訪問權(quán)限,有萬能的反射。
來呼盆,我們看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);
                    //反射調(diào)用覆寫的writeObject方法
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }

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

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

上面不是提到了自定義序列化方式嗎,除了覆寫2個方法外宿亡,還有一種方式常遂,就是實現(xiàn)Externalizable接口纳令。

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

4挽荠、transient關(guān)鍵字

上面介紹的都是自定義序列化方法,但是對于一個成員變量平绩,如果我不想對其進行序列化操作圈匆,這個時候如果僅采用自定義序列化,那么肯定要完全重寫ObjectOutputStream/ObjectInputStream內(nèi)的序列化/反序列化代碼捏雌,否則一旦調(diào)用defaultWriteObject/defaultReadObject跃赚,那么成員變量就自動進行了序列化的操作。
此時transient關(guān)鍵字就發(fā)揮作用了性湿。這個關(guān)鍵字會告訴defaultWriteObject方法纬傲,這個成員變量不用進行序列化了。如果一個成員變量被transient修飾符修飾了肤频,但是又想進行序列化怎么辦叹括?就重寫該類的writeObject /readObject方法吧,在這2個方法內(nèi)自定義序列化與反序列化宵荒。

5汁雷、序列化與反序列化

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

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

那么再進行反序列化調(diào)用readObject時暑刃,會發(fā)生什么呢厢漩?
會拋出ClassNotFoundException。為什么會這樣呢岩臣?因為序列化實際上會自動生成一個版本信息溜嗜,類似于這樣:

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

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

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

還有一種情況拨黔,就是父類實現(xiàn)了序列化接口,但是子類沒有绰沥。那么如果父類有變更篱蝇,子類的序列化版本會發(fā)生變化嗎?不會!(原因徽曲,我也不知道零截,這么亂,我也是醉了)

如果系統(tǒng)是使用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
        //調(diào)用ObjectOutputStream的默認序列化方法
        s.defaultWriteObject();
        //序列化位桶
        s.writeInt(buckets);
        //序列化當前對象的大小
        s.writeInt(size);
        //序列化每個entry內(nèi)的數(shù)據(jù)肮疗,即每個位桶內(nèi)的K-V
        internalWriteEntries(s);
}

其中:

//瞬態(tài)的size
transient int size;
//獲取當前實例的位桶容量
final int capacity() {
        return (table != null) ? table.length :
            (threshold > 0) ? threshold :
            DEFAULT_INITIAL_CAPACITY;
}
//遍歷所有位桶,逐個序列化位桶內(nèi)的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將位桶的數(shù)據(jù)用transient進行修飾:

transient Node<K,V>[] table;

進行這樣修飾之后伪货,也不是說在進行序列化時忽略掉们衙,而是基于一種優(yōu)化思想:在進行序列化時,如果該位桶有數(shù)據(jù)碱呼,我就進行序列化蒙挑,如果沒有數(shù)據(jù),那么我就不進行序列化愚臀。

再對readObject進行分析:

private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        //調(diào)用ObjectInputStream內(nèi)的默認方法進行反序列化
        s.defaultReadObject();
        //類似于進行反構(gòu)造
        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
            //隨機生成一個類似于負載因子的數(shù)
            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;

            //遍歷忆蚀,逐個反序列化
            //從這也可以看出,在進行反序列化的時候姑裂,不保證每個位桶內(nèi)的數(shù)據(jù)順序與原來的保持一致
            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馋袜、小結(jié)

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市捧毛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌让网,老刑警劉巖呀忧,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異溃睹,居然都是意外死亡而账,警方通過查閱死者的電腦和手機席爽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門绎橘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人痴柔,你說我怎么就攤上這事竞滓「篮穑” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵商佑,是天一觀的道長锯茄。 經(jīng)常有香客問我,道長茶没,這世上最難降的妖魔是什么肌幽? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮抓半,結(jié)果婚禮上喂急,老公的妹妹穿的比我還像新娘。我一直安慰自己笛求,他們只是感情好廊移,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布讥蔽。 她就那樣靜靜地躺著,像睡著了一般画机。 火紅的嫁衣襯著肌膚如雪冶伞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天步氏,我揣著相機與錄音响禽,去河邊找鬼。 笑死荚醒,一個胖子當著我的面吹牛芋类,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播界阁,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼侯繁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了泡躯?” 一聲冷哼從身側(cè)響起贮竟,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎较剃,沒想到半個月后咕别,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡写穴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年惰拱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啊送。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡偿短,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出馋没,到底是詐尸還是另有隱情昔逗,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布披泪,位于F島的核電站纤子,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏款票。R本人自食惡果不足惜控硼,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望艾少。 院中可真熱鬧卡乾,春花似錦、人聲如沸缚够。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至误堡,卻和暖如春古话,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锁施。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工陪踩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悉抵。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓肩狂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親姥饰。 傳聞我的和親對象是個殘疾皇子傻谁,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容