Java 對象序列化和反序列化

?????之前的文章中我們介紹過有關字節流字符流的使用,當時我們對于將一個對象輸出到流中的操作,使用DataOutputStream流將該對象中的每個屬性值逐個輸出到流中,讀出時相反。在我們看來這種行為實在是繁瑣,尤其是在這個對象中屬性值很多的時候。基于此,Java中對象的序列化機制就可以很好的解決這種操作。本篇就簡單的介紹Java對象序列化,主要內容如下:

  • 簡潔的代碼實現
  • 序列化實現的基本算法
  • 兩種特殊的情況
  • 自定義序列化機制
  • 序列化的版本控制

一、簡潔的代碼實現
?????在介紹對象序列化的使用方法之前,先看看我們之前是怎么存儲一個對象類型的數據的。

//簡單定義一個Student類
public class Student {

    private String name;
    private int age;

    public Student(){}
    public Student(String name,int age){
        this.name = name;
        this.age=age;
    }

    public void setName(String name){
        this.name = name;
    }
    public void setAge(int age){
        this.age = age;
    }
    public String getName(){
        return this.name;
    }
    public int getAge(){
        return this.age;
    }
    //重寫toString
    @Override
    public String toString(){
        return ("my name is:"+this.name+" age is:"+this.age);
    }
}
//main方法實現了將對象寫入文件并讀取出來
public static void main(String[] args) throws IOException{

        DataOutputStream dot = new DataOutputStream(new FileOutputStream("hello.txt"));
        Student stuW = new Student("walker",21);
        //將此對象寫入到文件中
        dot.writeUTF(stuW.getName());
        dot.writeInt(stuW.getAge());
        dot.close();

        //將對象從文件中讀出
        DataInputStream din = new DataInputStream(new FileInputStream("hello.txt"));
        Student stuR = new Student();
        stuR.setName(din.readUTF());
        stuR.setAge(din.readInt());
        din.close();

        System.out.println(stuR);
    }
輸出結果:my name is:walker age is:21

?????顯然這種代碼書寫是繁瑣的,接下來我們看看,如何使用序列化來完成保存對象的信息。

public static void main(String[] args) throws IOException, ClassNotFoundException {

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt"));
        Student stuW = new Student("walker",21);
        oos.writeObject(stuW);
        oos.close();

        //從文件中讀取該對象返回
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
        Student stuR = (Student)ois.readObject();
        System.out.println(stuR);
    }

?????寫入文件時,只用了一條語句就是writeObject,讀取時也是只用了一條語句readObject。并且Student中的那些set,get方法都用不到了。是不是很簡潔呢?接下來介紹實現細節。

二、實現序列化的基本算法
?????在這種機制中,每個對象都是對應著唯一的一個序列號,而每個對象在被保存的時候也是根據這個序列號來對應著每個不同的對象,對象序列化就是指利用了每個對象的序列號進行保存和讀取的。首先以寫對象到流中為例,對于每個對象,第一次遇到的時候會將這個對象的基本信息保存到流中,如果當前遇到的對象已經被保存過了,就不會再次保存這些信息,轉而記錄此對象的序列號(因為數據沒必要重復保存)。對于讀的情況,從流中遇到的每個對象,如果第一次遇到,直接輸出,如果讀取到的是某個對象的序列號,就會找到相關聯的對象,輸出。
?????說明幾點,一個對象要想是可序列化的,就必須實現接口 java.io.Serializable;,這是一個標記接口,不用實現任何的方法。而我們的ObjectOutputStream流,就是一個可以將對象信息轉為字節的流,構造函數如下:

public ObjectOutputStream(OutputStream out)

?????也就是所有字節流都可以作為參數傳入,兼容一切字節操作。在這個流中定義了writeObject和readObject方法,實現了序列化對象和反序列化對象。當然,我們也是可以通過在類中實現這兩個方法來自定義序列化機制,具體的后文介紹。此處我們只需要了解整個序列化機制,所有的對象數據只會保存一份,至于相同的對象再次出現,只保存對應的序列號。下面,通過兩個特殊的情況直觀的感受下他的這個基本算法。

三、兩個特殊的實例
?????先看第一個實例:

public class Student implements Serializable {

    String name;
    int age;
    Teacher t;  //另外一個對象類型

    public Student(){}
    public Student(String name,int age,Teacher t){
        this.name = name;
        this.age=age;
        this.t = t;
    }

    public void setName(String name){this.name = name;}
    public void setAge(int age){this.age = age;}
    public void setT(Teacher t){this.t = t;}
    public String getName(){return this.name;}
    public int getAge(){return this.age;}
    public Teacher getT(){return this.t;}
}

public class Teacher implements Serializable {
    String name;

    public Teacher(String name){
        this.name = name;
    }
}

public static void main(String[] args) throws IOException, ClassNotFoundException {

        Teacher t = new Teacher("li");
        Student stu1 = new Student("walker",21,t);
        Student stu2 = new Student("yam",22,t);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt"));
        oos.writeObject(stu1);
        oos.writeObject(stu2);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
        Student stuR1 = (Student)ois.readObject();
        Student stuR2 = (Student)ois.readObject();

        if (stuR1.getT() == stuR2.getT())
            System.out.println("相同對象");
    }

?????結果是很顯而易見的,輸出了相同對象。我們在main函數中定義了兩個student類型對象,他們卻都引用的同一個teacher對象在內部。完成序列化之后,反序列化出來兩個對象,通過比較他們內部的teacher對象是否是同一個實例,可以看出來,在序列化第一個student對象的時候t是被寫入流中的,但是在遇到第二個student對象的teacher對象實例時,發現前面已經寫過了,于是不再寫入流中,只保存對應的序列號作為引用。當然在反序列化的時候,原理類似。這和我們上面介紹的基本算法是一樣的。
?????下面看第二個特殊實例:

public class Student implements Serializable {

    String name;
    Teacher t;

}

public class Teacher implements Serializable {
    String name;
    Student stu;

}

public static void main(String[] args) throws IOException, ClassNotFoundException {

        Teacher t = new Teacher();
        Student s =new Student();
        t.name = "walker";
        t.stu = s;
        s.name = "yam";
        s.t = t;

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt"));
        oos.writeObject(t);
        oos.writeObject(s);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
        Teacher tR = (Teacher)ois.readObject();
        Student sR = (Student)ois.readObject();
        if(tR == sR.t && sR == tR.stu)System.out.println("ok");

    }

?????輸出的結果是ok,這個例子可以叫做:循環引用。從結果我們可以看出來,序列化之前兩個對象存在的相互的引用關系,經過序列化之后,兩者之間的這種引用關系是依然存在的。其實按照我們之前介紹的判斷算法來看,首先我們先序列化了teacher對象,因為他內部引用了student的對象,兩者都是第一次遇到,所以將兩者序列化到流中,然后我們去序列化student對象,發現這個對象以及內部的teacher對象都已經被序列化了,于是只保存對應的序列號。讀取的時候根據序列號恢復對象。

四、自定義序列化機制
?????綜上,我們已經介紹完了基本的序列化與反序列化的知識。但是往往我們會有一些特殊的要求,這種默認的序列化機制雖然已經很完善了,但是有些時候還是不能滿足我們的需求。所以我們看看如何自定義序列化機制。自定義序列化機制中,我們會使用到一個關鍵字,它也是我們之前在看源碼的時候經常遇到的,transient。將字段聲明transient,等于是告訴默認的序列化機制,這個字段你不要給我寫到流中去,我會自己處理的。、

public class Student implements Serializable {

    String name;
    transient int age;

    public String toString(){
        return this.name + ":" + this.age;
    }
}

public static void main(String[] args) throws IOException, ClassNotFoundException {

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt"));
        Student stu = new Student();
        stu.name = "walker";stu.age = 21;
        oos.writeObject(stu);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
        Student stuR = (Student)ois.readObject();

        System.out.println(stuR);
    }
輸出結果:walker:0

?????我們不是給age字段賦初始值了么,怎么會是0呢?正如我們上文所說的一樣,被transient修飾的字段不會被寫入流中,自然讀取出來就沒有值,默認是0。下面看看我們怎么自己來序列化這個age。

//改動過的student類,main方法沒有改動,大家可以往上看
public class Student implements Serializable {

    String name;
    transient int age;

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();

        oos.writeInt(25);
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();

        age = ois.readInt();
    }

    public String toString(){
        return this.name + ":" + this.age;
    }
}
輸出結果:walker:25

?????結果既不是我么初始化的21,也不是0,而是我們在writeObject方法中寫的25。現在我們一點一點看看每個步驟的意義。首先,要想要實現自定義序列化,就需要在該對象定義的類中實現兩個方法,writeObject和readObject,而且格式必須和上面貼出來的一樣,筆者試過改動方法修飾符,結果導致不能成功序列化。這是因為,Java采用反射機制,檢查該對象所在的類中有沒有實現這兩個方法,沒有的話就使用默認的ObjectOutputStream中的這個方法序列化所有字段,如果有的話就執行你自己實現的這個方法。
?????接下來,看看這兩個方法實現的細節,先看writeObject方法,參數是ObjectOutputStream 類型的,這個拿到的是我們在main方法中定義的ObjectOutputStream 對象,要不然它怎么知道該把對象寫到那個地方去呢?第一行我們調用的是oos.defaultWriteObject();這個方法實現的功能是,將當前對象中所有沒有被transient修飾的字段寫入流中,第二條語句我們顯式的調用了writeInt方法將age的值寫入流中。讀取的方法類似,此處不再贅述。

五、版本控制
?????最后我們來看看,序列化過程的的版本控制問題。在我們將一個對象序列化到流中之后,該對象對應的類的結構改變了,如果此時我們再次從流中將之前保存的對象讀取出來,會發生什么?這要分情況來說,如果原類中的字段被刪除了,那從流中輸出的對應的字段將會被忽略。如果原類中增加了某個字段,那新增的字段的值就是默認值。如果字段的類型發生了改變,拋出異常。在Java中每個類都會有一個記錄版本號的變量:static final serivalVersionUID = 115616165165L,此處的值只用于演示并不對應任意某個類。這個版本號是根據該類中的字段等一些屬性信息計算出來的,唯一性較高。每次讀出的時候都會去比較之前和現在的版本號確認是否發生版本不一致情況,如果版本不一致,就會按照上述的情形分別做處理。

?????對象的序列化就寫完了,如果有什么內容不妥的地方,希望大家指出!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • JAVA序列化機制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時候,保證對象的完整...
    時待吾閱讀 10,897評論 0 24
  • 官方文檔理解 要使類的成員變量可以序列化和反序列化,必須實現Serializable接口。任何可序列化類的子類都是...
    獅_子歌歌閱讀 2,423評論 1 3
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,721評論 18 399
  • 對象序列化的目標: 將對的字節序列對象永久的保存到磁盤中。 允許在網絡上直接傳輸對象,傳輸對象的字節序列。 對象序...
    年少懵懂丶流年夢閱讀 1,174評論 0 3
  • 一、 序列化和反序列化概念 Serialization(序列化)是一種將對象以一連串的字節描述的過程;反序列化de...
    步積閱讀 1,449評論 0 10