java I/O系統(9)-對象序列化與還原

引言

萬物皆對象,在我們程序運行中,對象只要在引用鏈上存在引用,那麼它就會一直存在。但是當我們程序結束的時候,那麼對象就會消亡。那麼在jvm不運行的時候我們仍能夠保存下來是非常有意義的,在java中可以用序列化來實現。序列化其實也是IO系統中的一部分。在本篇博文中,詳細介紹對象序列化的概念,不同序列化的方式和結果,並給出相應的demo。注意本文所說的序列化包括序列化與反序列化。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290

序列化概念

序列化是指把對象通過IO系統轉化成字節從而存儲在磁盤當中,需要的時候可以還原成對象。也就是對象持久化的一個方式。

序列化意義

對象的生命週期是隨着引用存在的,如果引用失效或者程序結束,那麼這個對象就不復存在。但是我們可以通過序列化的方式把對象轉成字節流從而存儲在磁盤當中,在任何我們需要的時候再從新恢復成一個完整的對象。換句話說,就是對象持久化存儲。
在我們網絡通信的過程中,我們可以把對象序列化之後把它放入網絡通信當中,這就彌補了不同操作系統之間的差異。不管你是從什麼機器上序列化產生的字節流,在任意存在jvm虛擬機的情況下都可以從新轉化成對象。

序列化設計IO

在序列化的過程中,根本上其實是一套IO操作,這個IO操作主要針對的就是對象。在IO系統中我們用ObjectInputStream與ObjectOutputStream來實現。在這兩個對象流中存在兩個方法readObejct和writeObject來實現對象的序列化輸出和反序列化寫入。

序列化方式

序列化是通過IO流來實現的,但是如果要序列化某一個對象,那麼這個對象必須要實現了Serializable或Externalizable接口,否則在IO流操作的時候會拋出異常。

也就是說序列化對象存在兩種方式:①Serializable;②Externalizable。那麼這兩個到底有什麼區別呢?對於前者來說是對象的全自動序列化,它對對象中的所有的屬性都會序列化,除卻transient關鍵字標記的屬性。對於後者來說,必須自己控制序列化過程,也就是說必須實現readExternal方法與writeExternal方法來控制序列化進行,同時後者反序列化的過程中,必須要執行對象的默認構造函數,所以說如果對象不存在可以調用的默認構造函數,那麼就會拋錯。

後面會詳細介紹兩者序列化的不同。

對象網

對象網是指序列化對象中對象之間引用的關係。比如說我序列化了A對象,A對象引用了B對象,B對象引用了C對象。那麼在序列化之後這個對象之間的關係也是一同會寫入字節流中進行持久化。在我們反序列化的過程中,能完整的還原出他們對象之間的關係。

transient關鍵字

transient是java的關鍵字,它表示在對象序列化的過程中,我們可以標記某一個敏感的屬性,要求它在序列化的過程中唯獨對這個屬性不序列化,也就是說不把這個敏感屬性持久化。
比如說在用戶系統當中,我們需要對用戶進行序列化存儲後進行通信,但是我們不希望暴露這個用戶的密碼屬性,那麼我們就可以對密碼這個屬性進行transient關鍵字標記:

 private transient String password;

Serializable序列化

在此處的代碼需要體現出以下關鍵點:①對象的序列化和反序列化;②transient標記屬性不會序列化;③能體現出對象的對象網關係;④反序列化不需要調用任何構造函數

假設存在一個用戶體系(user類),他們有自己各自的興趣(interest類)。在用戶體系中我們不希望在序列化的過程中暴露password字段,所以我們用transient標記它。接着我們對着兩個類的構造函數選擇包級別私有,如果反序列化需要調用構造函數那麼就會拋錯。同時在兩個類中我們都重寫toString方法,在測試的時候方便打印出完整的信息。下面就是這兩個類:


package com.brickworkers.io;

import java.io.Serializable;

public class User implements Serializable{

    private static final long serialVersionUID = 3667335206886584270L;

    private String name;

    private String phone;

    private transient String password;

    private Interest interest;


    //無參構造函數
    User() {
        name = "brickworker";
        phone = "157110";
        password = "123456";
    }

    User(String name, String phone, String password){
        this.name = name;
        this.password = password;
        this.phone = phone;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Interest getInterest() {
        return interest;
    }

    public void setInterest(Interest interest) {
        this.interest = interest;
    }

    @Override
    public String toString() {
        return "name:" + name + "  phone:"+ phone + "  password:" + password+ "  "+ interest;
    }
}

package com.brickworkers.io;

import java.io.Serializable;

public class Interest implements Serializable{

    private static final long serialVersionUID = -3147319655720895848L;

    private String name;

    private String description;

    Interest(String name, String description) {
        this.name = name;
        this.description = description;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    public String getDescription() {
        return description;
    }


    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "name:" + name+" description:" + description;
    }

}



接下來我們測試序列化和反序列化結果:

package com.brickworkers.io;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        //定義一個User類和Interest類對象
        User user = new User("brickworker", "110", "123456");
        Interest interest = new Interest("爬山", "爬山有益身心健康");
        user.setInterest(interest);

        //打印對象初始狀態
        System.out.println("對象初始狀態:");
        //重寫了toString方法,可以直接打印對象
        System.out.println(user);

        //輸出流,把對象通過序列化到磁盤
        //try-with-source,會自動關閉流
        try(ObjectOutputStream ops = new ObjectOutputStream(new FileOutputStream("F:/java/io/user.out"))){
            ops.writeObject(user);
        }



        //持久化結束之後,再進行反序列化,把對象恢復
        //try-with-source,會自動關閉流
        try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:/java/io/user.out"))){
            User readUser = (User)ois.readObject();
            //打印反序列化的對象結果
            System.out.println("反序列化之後的結果");
            System.out.println(readUser);
        }
    }
}


//輸出結果:
//對象初始狀態:
//name:brickworker  phone:110  password:123456  name:爬山 description:爬山有益身心健康
//反序列化之後的結果
//name:brickworker  phone:110  password:null  name:爬山 description:爬山有益身心健康
//
//

通過上面的代碼,我們可以看到①對象序列化和反序列化之後的結果是有所區別的,因爲password是transient的,所以序列化的時候這個屬性就被屏蔽了,並不會持久化到磁盤。②對象網還是存在,兩個對象的嵌套方式也得以還原。③對象還原的時候並不需要調用構造函數。

在這裏值得一提的是,如果僅僅user類實現了Serializable接口是不夠的,如果序列化的過程中存在對象網,那麼它所關聯的對象也必須要實現序列化,也就是說Interest也必須實現Serializable接口。還有一點,如果是繼承了父類,而父類是實現了Serializable接口的,那麼子類也默認實現該接口。

Serializable序列化方式如何控制序列化

在前面的代碼中提過一種控制方式了,就是用transient關鍵字標記的屬性不會被序列化。但是Serializable序列化方式還存在着別的控制方式。那就是直接在要序列化的對象中寫兩個方法(writeObject和readObject)。注意,這個不是重寫父類方法,只是Serializable序列化在IO流處理的時候,如果被序列化對象中本身就存在這兩個方法就優先調用它。

我們在User類中寫入writeObject和readObject方法,測試類還是原先的測試類:

package com.brickworkers.io;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.stream.Stream;

public class User implements Serializable{

    private static final long serialVersionUID = 3667335206886584270L;

    private String name;

    private String phone;

    private transient String password;

    private Interest interest;


    //無參構造函數
    User() {
        name = "brickworker";
        phone = "157110";
        password = "123456";
    }

    User(String name, String phone, String password){
        this.name = name;
        this.password = password;
        this.phone = phone;
    }

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

        name = ois.readUTF();
    }

    private void writeObject(ObjectOutputStream ops) throws IOException{
        ops.writeUTF(name);
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Interest getInterest() {
        return interest;
    }

    public void setInterest(Interest interest) {
        this.interest = interest;
    }

    @Override
    public String toString() {
        return "name:" + name + "  phone:"+ phone + "  password:" + password + " interest:" + interest;
    }
}

在這個類中我們加入了readObject和writeObject兩個獨立方法,在write方法中我們只序列化了name這一個熟悉,在readObject的時候也只處理這麼一個屬性。在測試直接用上面的測試例子,一個代碼都不用改,大家可以看看測試結果:

//對象初始狀態:
//name:brickworker  phone:110  password:123456 interest:name:爬山 description:爬山有益身心健康
//反序列化之後的結果
//name:brickworker  phone:null  password:null interest:null
//
//

可以在序列化的過程中只有name這麼一個字段被序列化了,其他的字段都沒有被序列化。這就是在實現Serializable接口第二種控制序列化的方法。

值得一說的是,看客們不用去糾結爲什麼會執行User類中的readObejct和writeObject方法,你只要知道在序列化過程中,在實現Serializable接口的情況下,IO操作會先判斷對象中有沒有這兩個方法,如果有就優先使用這兩個方法,同時如果你要實現它原本的序列化只需要執行default方法就行,像下面這樣:

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

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

Externalizable序列化

接下來我們說一說Externalizable序列化。實現Externalizable接口的序列化方式天然就需要自己控制序列化的屬性,在實現這個接口的時候必須要實現兩個方法:


    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // TODO Auto-generated method stub

    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // TODO Auto-generated method stub

    }

這個其實和我們前面說的Serializable接口序列化控制的第二種方法很像,需要在需要序列化對象中添加兩個方法,通過這2個方法對序列化對象的屬性控制。我們改寫User類,使它實現Externalizable接口,並書寫如上方法。在這個例子中,我們需要實現以下這些目標:①對象成功序列化和反序列化;②transient關鍵字修飾的屬性是否被序列化。③能體現出對象的對象網關係;④反序列化必須要調用默認構造函數。

修改之後的User類:

package com.brickworkers.io;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.stream.Stream;

public class User implements Externalizable{


    private String name;

    private String phone;

    private transient String password;

    private Interest interest;


    //無參構造函數
    User() {
        name = "brickworker";
        phone = "157110";
        password = "123456";
    }

    User(String name, String phone, String password){
        this.name = name;
        this.password = password;
        this.phone = phone;
    }


    @Override
    public void writeExternal(ObjectOutput out) throws IOException {

        out.writeUTF(name);
        out.writeUTF(phone);
        out.writeUTF(password);
        out.writeObject(interest);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        phone = in.readUTF();
        password = in.readUTF();
        interest = (Interest) in.readObject();

    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Interest getInterest() {
        return interest;
    }

    public void setInterest(Interest interest) {
        this.interest = interest;
    }

    @Override
    public String toString() {
        return "name:" + name + "  phone:"+ phone + "  password:" + password + " interest:" + interest;
    }


}

修改之後的Interest類:

package com.brickworkers.io;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Interest implements Externalizable{


    private String name;

    private String description;

    Interest(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public Interest() {

        name = "爬山";
        description = "爬山有益身心健康";
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeUTF(description);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        description = in.readUTF();

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    public String getDescription() {
        return description;
    }


    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "name:" + name+" description:" + description;
    }


}

測試類不用修改,直接進行測試,你會發現拋錯了:
Exception in thread “main” java.io.InvalidClassException: com.brickworkers.io.User; no valid constructor
這個錯誤告訴你在User對象中沒有默認的構造器,仔細觀察上面的代碼,你會發現兩個構造器我都是用default來實現的,所以是不可訪問的構造器,所以我們先要把user類和interest類的無參構造器設置成public。
在這裏需要注意以下幾點:①序列化對象必須要有可調用的顯式無參構造器或者默認構造器;②序列化對象的參數構造器無影響;③對象網中的其他對象也必須要有顯式無參構造器或者默認構造器

把User類中的無參構造器換成public就可以順利進行測試,測試結果如下:

//對象初始狀態:
//name:brickworker  phone:110  password:123456 interest:name:爬山 description:爬山有益身心健康
//反序列化之後的結果
//name:brickworker  phone:110  password:123456 interest:name:爬山 description:爬山有益身心健康
//
//


從測試結果我們可以看出,①序列化和反序列化都是成功的,對象成功還原;②transient關鍵字修飾的對象也被正常序列化;③有完整的對象網

所以,transient關鍵字只有在Serializable默認的自動序列化中才會生效。

值得注意的是,在IO系統中writeXXX和readXXX是有順序可言的,比如說在Interest類中兩個方法是如下這麼實現的:

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeUTF(description);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        description = in.readUTF();
         name = in.readUTF();

    }

先寫入name,再寫入description,但是在讀取的時候先讀取description,再讀取name。這是錯誤的,因爲寫入和讀取時有順序的,讀取必須要按照寫入的順序讀寫,不然結果就會是錯誤的,務必謹記!

如果對導出的user.out的內容有興趣的,可以下載一個winhex軟件,它是一個二進制碼文。

關於實現Serializable接口之後設立的serialVersionUID,它其實對版本進行控制,通過比較serialVersionUID可以確定是否可以成功的反序列化。這一塊內容篇幅問題不展開討論,有興趣的可以自己探究。

好了,基本上已經把我自己知道的所有序列化都寫完了,最後,我再說一點有意思的東西,在存在對象網的序列化中,支持兩種序列化方式混合使用,也就是說User類你可以實現Externalizable接口,但是Interest可以實現Serializable接口。希望對你下次面試有所幫助。

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