Java序列化機制

Java序列化機制


序列化和反序列化

Java序列化是Java內(nèi)建的數(shù)據(jù)(對象)持久化機制很洋,通過序列化可以將運行時的對象數(shù)據(jù)按照特殊的格式存儲到文件中溶耘,以便在將來重建對象。

Java序列化是Java IO的一部分,需要ObjectStream提供支持。支撐序列化機制的基礎(chǔ)是運行期對對象的信息(如類信息)和數(shù)據(jù)(如成員域)的讀取和存儲肋坚,從而將數(shù)據(jù)的字節(jié)流寫入文件或字節(jié)數(shù)組中。反序列化過程則是讀取字節(jié)流中對象的描述信息和數(shù)據(jù)肃廓,通過指定類的構(gòu)造器和持久化的數(shù)據(jù)動態(tài)地重建出對象。

當然诲泌,對于一般對象而已盲赊,重建后的對象和被序列化到文件中的對象,不會是同一個了(地址)敷扫,因為可能都不在同一臺計算機中哀蘑,序列化中的“內(nèi)存地址”這個對象相等的標志,變成了對象的序列號葵第,這也是序列化的名稱由來绘迁。不過對于枚舉對象,因為在反序列化過程做了特殊的處理卒密,保證了對象的唯一性缀台。

需要注意的是,序列化是保存對象的狀態(tài)哮奇,和類狀態(tài)無關(guān)膛腐,所以它不會讀取靜態(tài)數(shù)據(jù)。


序列化和反序列化的序列號

序列號是關(guān)聯(lián)到對象的鼎俘,是內(nèi)部流處理的機制哲身,外部是不可見的。

  1. 序列化
  • 序列化的每個對象對應(yīng)一個序列號贸伐,這個序列號是唯一對應(yīng)一個對象的勘天,相當于對象內(nèi)存地址的作用;
  • 對于每個首次遇到的對象捉邢,會將其數(shù)據(jù)存儲到輸出流中脯丝;
  • 對于相同的對象(這個相同的判斷是通過地址),會引用之前的對象的序列號歌逢,相當于引用一個地址巾钉;
  1. 反序列化(這個過程序列號就是對象的唯一標識符了)
  • 對于首次遇到的序列號,會根據(jù)流中的信息構(gòu)建一個Java對象秘案;
  • 對于與之前的序列號相同的序列號砰苍,直接引用之前構(gòu)建的對象(這里是對象的內(nèi)存地址)

序列化的版本號

序列化的版本號是在類中定義的private static final long serialVersionUID潦匈,這個字段用來標示類的版本,它是數(shù)據(jù)域類型和方法簽名信息通過SHA算法取到的指紋赚导,采用了SHA碼的前8個字節(jié)茬缩,主要用在反序列化過程中的類的校驗。如果在反序列化過程中吼旧,當前類的serialVersionUID和序列化文件中的serialVersionUID不一致凰锡,那么就無法從流中構(gòu)建對象,并拋出異常圈暗。
那么什么時候這個serialVersionUID會變呢掂为?
主要有下面兩種情況

  • 在實現(xiàn)Serializable接口時,沒有定義serialVersionUID屬性员串,但在序列化之后勇哗,修改了類結(jié)構(gòu);
  • 手動修改了serialVersionUID的值寸齐;

serialVersionUID是用來應(yīng)對類在序列化之后類發(fā)生了變更的情況欲诺。對于第一個情況,在定義可序列化類時沒有定義serialVersionUID渺鹦,不會影響序列化過程扰法,因為在序列化過程中會自動生成一個寫入到流中,如果在之后沒有修改類的任何域或方法毅厚,反序列化是沒有問題的塞颁,(因為生成指紋的基礎(chǔ)沒變,自動生成的指紋即serialVersionUID是一致的)卧斟。但是如果在序列化之后修改了類殴边,反序列化就會失敗,除非手動加上serialVersionUID值和前面自動生成的值一致珍语。查看舊值的方法是:使用jdk的命令serialver <類名>锤岸,能拿到他早期版本的版本號。

對于第二種情況板乙,一般而言是偷,除非是有此類需求,不然不會手動去破壞反序列化募逞。


相關(guān)的類蛋铆、接口

序列化標識接口

  • java.io.Externalizable
  • java.io.Serializable

序列化機制實現(xiàn)類

  • java.io.ObjectInputStream
  • java.io.ObjectOutputStream

實現(xiàn)Serializable接口或者Externalizable接口的才可以被序列化。Serializable接口沒有任何方法放接,是一個標記接口刺啦,Externalizable接口是Serializable接口的子接口,擁有兩個公共方法纠脾,用來自定義序列化過程玛瘸。

Serializable和Externalizable的區(qū)別

實現(xiàn)了Externalizable接口的類蜕青,需要實現(xiàn)接口的兩個抽象方法,readExternal和writeExternal糊渊,需要讀寫哪些數(shù)據(jù)都需要顯式的調(diào)用流的讀寫方法右核,也就是序列化的任務(wù)從Java內(nèi)建轉(zhuǎn)移到了開發(fā)者,提供了更大自由度的同時渺绒,也提高了復(fù)雜度贺喝。而且在Externalizable反序列化時,會調(diào)用類的public無參構(gòu)造器宗兼,如果類中沒有定義無參構(gòu)造器(沒有重載構(gòu)造器的話躏鱼,會有默認的無參構(gòu)造器的),會拋出異常殷绍。后面再詳細說明此接口的使用挠他。

對象流是序列化的核心,readObject和writeObject方法從文件中反序列化對象和將對象序列化到文件中篡帕。


使用默認的序列化

以下是實現(xiàn)了Serializable接口的類,main方法演示了對象的序列化和反序列化的使用贸呢。

public class SerializableUser implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    private SerializableUser(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public SerializableUser() {
    }

    @Override
    public String toString() {
        return "User  -> name:" + name + " ,age=" + age;

    }

    public static void main(String[] args) throws IOException {
        SerializableUser user = new SerializableUser("harry", 19);
        SerialUtil.writeToFile("ser", user);
        SerializableUser u = (SerializableUser) SerialUtil.readFromFile("ser");
        System.out.println(u);
        // Files.deleteIfExists(Paths.get("ser"));
    }
}

// 工具類

public class SerialUtil {
    public static void writeToFile(String path, Object obj) {
        ObjectOutputStream oos;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(path));
            oos.writeObject(obj);
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Object readFromFile(String path) {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(path));
            return ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (ois != null)
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
        return null;

    }

結(jié)果:

User  -> name:harry ,age=19
序列化文件內(nèi)容:
aced 0005 7372 0026 6a64 6b2e 7465 7374
2e73 6572 6961 6c69 7a61 626c 652e 5365
7269 616c 697a 6162 6c65 5573 6572 0000
0000 0000 0001 0200 0249 0003 6167 654c
0004 6e61 6d65 7400 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b78 7000 0000
1374 0005 6861 7272 79

自定義序列化

想要修改Serializable接口實現(xiàn)類的序列化邏輯镰烧,比如有些字段不需要寫入,或者自己想控制寫入的過程楞陷,如想給寫入文件的字節(jié)流加密等等怔鳖,就不能直接使用對象流提供的read和write方法了。

transient關(guān)鍵字

使用transient關(guān)鍵字修飾的字段固蛾,在序列化時會被忽略结执,不會寫入流中。transient意思是瞬時的艾凯,既然不是長久的献幔,就代表不要持久化。

重寫writeObject()和readObject()

在實現(xiàn)類中重寫writeObject和readObject方法可以改變序列化的方法調(diào)用趾诗,會調(diào)用類中重寫的方法蜡感,而不會再走對象流的方法,實現(xiàn)邏輯是在對象流的writeObject和readObject中通過反射判斷類中是否重寫了方法恃泪,然后反射調(diào)用郑兴。
一般重寫的邏輯是先調(diào)用流的defaultWriteObject和defaultReadObject方法,然后追加自己的寫入邏輯贝乎。這兩個default方法是原本對象流的writeObject和readObject方法會調(diào)用的情连,所以具備默認的讀寫作用±佬В可以參考ArrayList類的設(shè)計却舀,ArrayList內(nèi)部使用數(shù)組來存儲元素虫几,且數(shù)組是會動態(tài)擴容的,如果直接序列化數(shù)組禁筏,會序列化很多的空元素即null到流中持钉,既浪費空間也降低了效率,所以在類中將數(shù)組變量標注成transient篱昔,然后在重寫writeObject和readObect方法每强,將實際的元素寫入對象流和從對象流中讀出。

readResolve()

這個方法是應(yīng)對enum出現(xiàn)之前設(shè)計的枚舉的代替代碼和單例代碼的措施州刽。
看一段示例:

public class SimulateEnum {

    /**
     * 模擬一個枚舉類空执,在Java內(nèi)建枚舉出現(xiàn)之前的情況
     */
    static class MyEnum implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private static int order = 0;
        private int ordernum;

        private MyEnum(String name) {
            this.name = name;
            this.ordernum = order++;
        }

        public String getName() {
            return name;
        }

        public int getOrdernum() {
            return ordernum;
        }

        public static final MyEnum A = new MyEnum("A");
        public static final MyEnum B = new MyEnum("B");
        public static final MyEnum C = new MyEnum("C");
    }

    public static void main(String[] args) throws IOException {
        MyEnum a = MyEnum.C;
        System.out.println(a.getName());
        System.out.println(a.getOrdernum());
        SerialUtil.writeToFile("enum", a);
        MyEnum rebuilda = (MyEnum) SerialUtil.readFromFile("enum");
        System.out.println(rebuilda.getName());
        System.out.println(rebuilda.getOrdernum());
        System.out.println(a == rebuilda);
        Files.deleteIfExists(Paths.get("enum"));
    }
}

在示例中我們模擬了一個枚舉的實現(xiàn),我們定義了私有的構(gòu)造器和幾個作為枚舉對象的靜態(tài)對象穗椅,我們期待的是枚舉是安全的辨绊,不會再生成除了定義的三個變量之外的其他變量,但是反序列化過程會破壞這種設(shè)計匹表∶趴溃看結(jié)果:

C
2
C
2
false

因為對象的反序列化會創(chuàng)建新的對象的,即使類的構(gòu)造器是私有的袍镀,這會破壞單例模式的設(shè)計默蚌。為了去保護在Java枚舉出現(xiàn)之前的模擬枚舉,對象留提供了一個解決措施——在類中實現(xiàn)readResolve方法苇羡。在readResolve方法中攔截新對象的生成绸吸,使之返回已有的對象。
我們在MyEnum類中加入以下方法:

private Object readResolve() throws Exception {
            switch (name) {
            case "A":
                return A;
            case "B":
                return B;
            case "C":
                return C;
            default:
                throw new Exception();
            }

        }

再執(zhí)行返回的結(jié)果就是true了设江。
那么為什么在反序列真正的枚舉的時候就不用再考慮在類中實現(xiàn)readReslove方法呢锦茁,原因是ObjectStream對枚舉做了支持〔娲妫看一下調(diào)用棧码俩。

readObejct()-> readObject0()-> checkResolve(readEnum(unshared))->readEnum():

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

看一下源碼,實際上和我們自己寫的readResolve里面的意思是差不多的歼捏。

Externalizable接口

一個實現(xiàn)Externalizable接口的類表明它是可被外部化的握玛,也是可序列化,只是這個序列化的責任轉(zhuǎn)移給了外部控制甫菠。這個接口的定義:

public interface Externalizable extends java.io.Serializable {
    
    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

兩個抽象方法給開發(fā)者自行實現(xiàn)序列化的處理挠铲。
示例:

public class ExternalizableUser implements Externalizable {

    private String name;
    private int age;

    private ExternalizableUser(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public ExternalizableUser() {
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = in.readInt();
    }

    @Override
    public String toString() {
        return "User  -> name:" + name + " ,age=" + age;

    }

    public static void main(String[] args) throws IOException {
        ExternalizableUser user = new ExternalizableUser("harry",19);
        SerialUtil.writeToFile("ext", user);
        ExternalizableUser u = (ExternalizableUser) SerialUtil.readFromFile("ext");
        System.out.println(u);
        Files.deleteIfExists(Paths.get("ext"));
    }
}
//User  -> name:harry ,age=19

如果上面示例中的writeExternal和readExternal方法沒有實現(xiàn),那么返回的對象是一個空的對象寂诱,數(shù)據(jù)是默認值拂苹。
序列化的文件內(nèi)容是:

aced 0005 7372 0028 6a64 6b2e 7465 7374
2e73 6572 6961 6c69 7a61 626c 652e 4578
7465 726e 616c 697a 6162 6c65 5573 6572
c8f9 50ef 76db 2f15 0c00 0078 7074 0005
6861 7272 7977 0400 0000 1378 

感興趣的同學可以去研究一下序列化文件的格式。

The end

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市瓢棒,隨后出現(xiàn)的幾起案子浴韭,更是在濱河造成了極大的恐慌,老刑警劉巖脯宿,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件念颈,死亡現(xiàn)場離奇詭異,居然都是意外死亡连霉,警方通過查閱死者的電腦和手機榴芳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來跺撼,“玉大人窟感,你說我怎么就攤上這事∏妇” “怎么了柿祈?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長哩至。 經(jīng)常有香客問我躏嚎,道長,這世上最難降的妖魔是什么菩貌? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任紧索,我火速辦了婚禮,結(jié)果婚禮上菜谣,老公的妹妹穿的比我還像新娘。我一直安慰自己晚缩,他們只是感情好尾膊,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荞彼,像睡著了一般冈敛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸣皂,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天抓谴,我揣著相機與錄音,去河邊找鬼寞缝。 笑死癌压,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的荆陆。 我是一名探鬼主播滩届,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼被啼!你這毒婦竟也來了帜消?” 一聲冷哼從身側(cè)響起棠枉,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泡挺,沒想到半個月后辈讶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡娄猫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年贱除,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稚新。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡勘伺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出褂删,到底是詐尸還是另有隱情飞醉,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布屯阀,位于F島的核電站缅帘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏难衰。R本人自食惡果不足惜钦无,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盖袭。 院中可真熱鬧失暂,春花似錦、人聲如沸鳄虱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拙已。三九已至决记,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間倍踪,已是汗流浹背系宫。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留建车,地道東北人扩借。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像缤至,于是被迫代替她去往敵國和親往枷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

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

  • JAVA序列化機制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時候,保證對象的完整...
    時待吾閱讀 10,864評論 0 24
  • java的序列化機制支持將對象序列化為本地文件或者通過網(wǎng)絡(luò)傳輸至別處, 而反序列化則可以讀取流中的數(shù)據(jù), 并將其轉(zhuǎn)...
    Ten_Minutes閱讀 393評論 0 1
  • 張小凡蹲在地上扔石頭,他記得自己穿越之前是在泡澡错洁,用綠茶泡澡秉宿,清香解乏。泡著泡著他就睡著了屯碴!一睜眼描睦,他就發(fā)現(xiàn)自己穿...
    哇呵呵a閱讀 420評論 0 2
  • 2018.4.11 【閱讀打卡】 Day48---【閱讀一小時】 時間管理四象限: 第一優(yōu)先緊急重要工作 第二優(yōu)先...
    Karen娟兒閱讀 263評論 0 1
  • 七律·豐收(新韻) 賀信鶯聲喜氣揚今艺,潮升雪化漲春江韵丑; 起飛雛燕戲田地,黃透枇杷綴麥場虚缎; 墩就高岡顆粒小撵彻,集成經(jīng)典字...
    補缺樓丨胡德棒閱讀 1,628評論 0 2