1. 程式人生 > >java I/O系統(9)-物件序列化與還原

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介面。希望對你下次面試有所幫助。