Java I/O系統學習系列五:Java序列化機制

  在Java的世界裏,建立好對象以後,只要須要,對象是能夠長駐內存,可是在程序終止時,全部對象仍是會被銷燬。這其實很合理,可是即便合理也不必定能知足全部場景,仍然存在着一些狀況,須要可以在程序不運行的狀況下保持對象,因此序列化機制應運而生。java

1. 爲何要有序列化

  簡單來講序列化的做用就是將內存中的對象保存起來,在須要時能夠重建該對象,而且重建後的對象擁有與保存以前的對象所擁有的信息相同。在實際應用中,對象序列化常會用在以下場景:數據庫

  • RPC框架的數據傳輸;
  • 對象狀態的持久化保存;

  也許你會以爲,要達到這種持久化的效果,咱們直接將信息寫入文件或數據庫也能夠實現啊,爲何還要序列化?這是一個好問題,試想若是咱們採用前面所述的方法,在序列化對象和反序列化恢復對象時,咱們必須考慮如何完整的保存和恢復對象的信息,這裏面會涉及到不少繁瑣的細節,稍加不注意就可能致使信息的丟失。若是可以有一種機制,只要將一個對象聲明爲是「持久性」的,就可以爲咱們處理掉全部細節,這樣豈不是很方便,這就是序列化要作的事情。Java已經將序列化的概念加入到語言中,本文的關於序列化的全部例子都是基於Java的。數組

  Java提供的原生序列化機制功能強大,有其本身的一些特色:網絡

  • 序列化處理很是簡單,只需將對象實現Serializable接口便可;
  • 可以自動彌補不一樣操做系統之間的差別,便可以在運行Windows系統的計算機上建立一個對象,將其序列化,而後經過網絡將它發送給一臺運行Unix系統的計算機,而後在那裏準確地從新組裝,沒必要擔憂數據在新的機器上表示會不一樣;
  • 對象序列化不只會保存對象的「全景圖」,並且可以追蹤對象內所包含的全部引用,並保存那些對象;接着又能對對象內包含的每一個這樣的引用進行追蹤,依此類推;
  • 對象序列化保存的是對象的」狀態」,即它的成員變量,因此對象序列化並不會處理類中的靜態變量;

2. 序列化機制的使用

  Java的對象序列化機制是將那些實現了Serializable接口的對象轉換成一個字節序列,並可以在之後將這個字節序列徹底恢復爲原來的對象。app

  要序列化一個對象,首先要建立一個OutputStream對象,而後將其封裝在一個ObjectOutputStream對象內,接着只需調用writeObject()方法便可將對象序列化,並將序列化後的字節序列發送給OutputStream。要將一個序列還原爲一個對象,則須要將一個InputStream封裝在ObjectInputStream內,而後調用readObject(),該方法會返回一個引用,它指向一個向上轉型的Object,必須向下轉型才能直接使用。框架

  咱們來看一個例子,如何序列化和反序列化對象。dom

public class Worm implements Serializable{
    private static Random rand = new Random(47);
    private Data[] d = {
        new Data(rand.nextInt(10)),
        new Data(rand.nextInt(10)),
        new Data(rand.nextInt(10))
    };
    private Worm next;
    private char c;
    public Worm(int i, char x){
        System.out.println("Worm constructor: " + i);
        c = x;
        if(--i > 0){
            next = new Worm(i,(char)(x + 1));
        }
    }
    public Worm(){
        System.out.println("Default constructor");
    }
    public String toString(){
        StringBuilder result = new StringBuilder(":");
        result.append(c);
        result.append("(");
        for(Data dat : d){
            result.append(dat);
        }
        result.append(")");
        if(next != null){
            result.append(next);
        }
        return result.toString();
    }
    public static void main(String[] args) throws ClassNotFoundException, IOException{
        Worm w = new Worm(6,'a');
        System.out.println("w = " + w);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));
        out.writeObject("Worm storage\n");
        out.writeObject(w);
        out.close();
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"));
        String s = (String)in.readObject();
        Worm w2 = (Worm)in.readObject();
        System.out.println(s + "w2 = " + w2);
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream out2 = new ObjectOutputStream(bout);
        out2.writeObject("Worm storage\n");
        out2.writeObject(w);
        out2.flush();
        ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
        s = (String)in2.readObject();
        Worm w3 = (Worm)in2.readObject();
        System.out.println(s + "w3 = " + w3);
    }
}

class Data implements Serializable{
    private int n;
    public Data(int n){this.n = n;}
    public String toString(){return Integer.toString(n);}
}

  輸出結果以下:函數

Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w2 = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w3 = :a(853):b(119):c(802):d(788):e(199):f(881)

  這段代碼經過對連接的對象生成一個worm(蠕蟲)對序列化機制進行測試,每一個對象都與worm中的下一段連接,同時又與屬於不一樣類(Data)的對象引用數組連接。測試

  對象序列化不只保存了對象的「全景圖」,並且能追蹤對象內所包含的全部引用,並保存那些對象;還能對對象內包含的每一個這樣的引用進行追蹤;依此類推。ui

  並且從上面的輸出結果還能夠看出一個Serializable對象進行還原的過程當中,沒有調用任何構造器,包括默認的構造器。整個對象都是經過從InputStream中取得數據恢復而來的。

3. 序列化須要什麼

  前面咱們有說到序列化的目的之一是支持rpc框架的數據傳輸,好比咱們將一個對象序列化,並經過網絡將其傳給另外一臺計算機,另外一臺計算機經過反序列化來還原這個對象,那是否只須要該序列化文件就能還原對象呢?咱們用下面的代碼來測試一下。

public class Serialize implements Serializable{}
}

public class FreezeSerialize {
    public static void main(String[] args) throws Exception{
        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("X.file"));
        Serialize serialize = new Serialize();
        os.writeObject(alien);
    }
}

public class ThawSerialize {
    public static void main(String[] args) throws Exception{
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("X.file"));
        Object mystery = in.readObject();
        System.out.println(mystery.getClass());
    }
}

  FreezeSerialize類用來把對象序列化到文件中,ThawSerialize類用來反序列化對象,在測試電腦上若是同時執行這兩個類是沒有問題的,但若是咱們將Serialize類的位置更改一下(或者直接將FreezeSerialize和Serialize刪掉),則執行ThawSerialize反序列化時會報錯誤--ClassNotFoundException,因此能夠知道,反序列化時是須要原對象的Class對象的。

  既然反序列化時須要對應的Class對象,那若是序列化時和反序列化時對應的Class版本不同會怎麼樣呢(這種狀況是存在的)?爲了模擬這種狀況,咱們先執行FreezeSerialize類的main方法,再給Serialize類添加一個屬性,這時再跑一下ThawSerialize類的main方法,能夠發現報java.io.InvalidClassException異常,明明是能經過編譯的可是卻報錯了,這種狀況有沒有什麼辦法解決呢?有的,咱們能夠給實現了Serializable接口的類添加一個long類型的成員:serialVersionUID,修飾符爲private static final,而且指定一個隨機數便可。

  這個serialVersionUID其實叫序列化版本號,若是不指定的話,編譯器會在編譯後的class文件中默認添加一個,其值是根據當前類結構生成。可是這樣會帶來一個問題,若是類的結構發生了改變,那編譯以後對應的版本號也會發生改變,而虛擬機是否容許反序列化,不只取決於類路徑和功能代碼是否一致,還有一個很是重要的一點是兩個類的序列化ID是否一致,若是不一致則不容許序列化而且會拋出InvalidClassException異常,這就是前面不添加序列號時更改類結構再反序列化時會報錯的緣由。因此建議給實現了Serializable接口的類添加一個序列化版本號serialVersionUID,並指定值。

  關於序列化版本號還有一個點須要主意,版本號一致的狀況下,若待反序列化的對象與當前類現有結構不一致時,則採用兼容模式,即:該對象的屬性現有類有的則還原,沒有的則忽略。

4. 序列化控制

  上面咱們咱們使用的是Java提供的默認序列化機制,即將對象成員所有序列化。可是,若是有特殊的須要呢?好比,只但願對象的某一部分被序列化。在這種特殊狀況下,能夠經過若干種方法來實現想要的效果,下面一一介紹。

4.1 實現Externalizable接口

  經過實現Externalizable接口(代替實現Serializable),能夠對序列化過程進行控制。這個Externalizable接口繼承了Serializable接口,同時增長了兩個方法:writeExternal()和readExternal()。這兩個方法會分別在序列化和反序列化還原的過程當中被自動調用,這樣就能夠在這兩個方法種指定執行一些特殊操做。下面來看一個簡單例子:

public class Blip implements Externalizable{
    private int i;
    private String s;
    public Blip(){
        System.out.println("Blip Constructor");
    }
    public Blip(String x, int a){
        System.out.println("Blip(String x, int a)");
        s = x;
        i = a;
    }
    public String toString(){
        return s + i;
    }
    public void writeExternal(ObjectOutput out) throws IOException{
        System.out.println("Blip.writeExternal");
        out.writeObject(s);
        out.writeInt(i);
    }
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{
        System.out.println("Blip.readExternal");
        s = (String)in.readObject();
        i = in.readInt();
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException{
        System.out.println("Constructing objects:");
        Blip b = new Blip("A String ",47);
        System.out.println(b);
        // 序列化對象
        ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Blip.out"));
        System.out.println("Saving object:");
        o.writeObject(b);
        o.close();
        // 還原對象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip.out"));
        System.out.println("Recovering b:");
        b = (Blip)in.readObject();
        System.out.println(b);
    } 
}

  在這個例子中,對象繼承了Externalizable接口,成員s和i只在第二個構造器中初始化(而不是默認構造器),咱們在writeExternal()方法中對要序列化保存的成員執行寫入操做,在readExternal()方法中將其恢復。輸出結果以下:

Constructing objects:
Blip(String x, int a)
A String 47
Saving object:
Blip.writeExternal
Recovering b:
Blip Constructor
Blip.readExternal
A String 47

  這裏須要注意的幾個點:

  1. 對象實現了Externalizable以後,沒有任何成員能夠自動序列化,須要在writeExternal()內部只對所需部分進行顯式的序列化,而且在readExternal()方法中將其恢復。
  2. 在將實現了Externalizable接口的對象進行反序列化操做時,會調用其默認構造函數,若是沒有,則會報錯java.io.InvalidClassException。因此若是對象實現了Externalizable接口,則還須要檢查其是否有默認構造函數。

4.2 transient(瞬時)關鍵字

  在咱們對序列化進行控制時,可能會碰到某個特定子對象不想讓Java的序列化機制自動保存與恢復。好比一些敏感信息(密碼),即便對象中的這些成員是由private修飾,一經序列化處理,經過讀取文件或者網絡抓包的方式仍是能訪問到它。

  前面說的經過實現Externalizable接口能夠解決這個問題,可是假如對象有不少的成員,而咱們只但願其中少許成員不被序列化,那經過實現Externalizable接口的方式就不合適了(由於須要在writeExternal()方法中作大量工做),這種狀況下,transient關鍵就能夠大顯身手了。在實現了Serializable接口的類中,被transient關鍵字修飾的成員是不會被序列化的。並且,因爲Externalizable對象在默認狀況下不會序列化對象的任何字段,transient關鍵字只能和Serializable對象一塊兒使用。

4.3 Externalizable的替代方法

  除了上面兩種方法,還有一種相對不那麼「正規」的辦法--咱們能夠實現Serializable接口,並添加名爲writeObject()和readObject()的方法。當對象被序列化或者被反序列化還原時,就會自動地分別調用這兩個方法(只要咱們提供了這兩個方法,就會使用它們而不是默認的序列化機制)。可是須要注意的是這兩個方法必須有準確的方法特徵簽名:

private void writeObject(ObjectOutputStream stream) throws IOException;

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;

  在調用ObjectOutputStream.writeObject()時,會檢查所傳遞的Serializable對象,看看是否實現了它本身的writeObject()。若是是,就跳過正常的序列化過程並調用對象本身的writeObject()方法。readObject()的狀況是相似的。這就是這種方式的原理。

  還有一個技巧,在咱們提供的writeObject()內部,能夠調用defaultWriteObject()來選擇執行默認的writeObject()。相似,在readObject()內部,咱們能夠調用defaultReadObject()。下面看一個例子,如何對一個Serializable對象的序列化與恢復進行控制:

public class SerialCtl implements Serializable{
    
    private String noTran;
    private transient String tran;
    public SerialCtl(String noTran, String tran){
        this.noTran = "Not Transient: " + noTran;
        this.tran = "Transient: " + tran;
    }
    public String toString(){ return noTran + "\n" + tran; }
    private void writeObject(ObjectOutputStream stream) throws IOException {
        stream.defaultWriteObject();
        stream.writeObject(tran);
    }
    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException{
        stream.defaultReadObject();
        tran = (String)stream.readObject();
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException{
        SerialCtl sc = new SerialCtl("papaya","mango");
        System.out.println("Before:\n" + sc);
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        ObjectOutputStream o = new ObjectOutputStream(buf);
        o.writeObject(sc);
        // 還原
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
        SerialCtl sc2 = (SerialCtl)in.readObject();
        System.out.println("After:\n" + sc2);
    }

}

  輸出結果:

Before:
Not Transient: papaya
Transient: mango
After:
Not Transient: papaya
Transient: mango

  在這個例子中,有一個String字段是普通字段,而另外一個是transient字段,對比證實非transient字段由defaultWriteObject()方法保存,而transient字段必須在程序中明確保存和恢復。

  在writeObject()內部第一行調用defaultWriteObject()方法是爲了利用默認序列化機制序列化對象的非transient成員,一樣,在readObject()內部第一行調用defaultReadObject()方法是爲了利用默認機制恢復非transient成員。注意,必須是第一行調用。

5. 再深刻一點

  使用序列化的一個主要目的是存儲程序的一些狀態,以便咱們後面能夠容易地將程序恢復到當前狀態。在這樣作以前,咱們先考慮幾種狀況。若是咱們將兩個對象(它們都包含有有指向第三個對象的引用成員)進行序列化,會發生什麼狀況?當咱們從它們的序列化文件中恢復這兩個對象時,第三個對象會只出現一次嗎?若是將這兩個對象序列化成獨立的文件,而後在代碼的不一樣部分對它們進行反序列化還原,又會怎樣呢?先看例子:

public class MyWorld {

    public static void main(String[] args) throws IOException, ClassNotFoundException{
        House house = new House();
        List<Animal> animals = new ArrayList<Animal>();
        animals.add(new Animal("Bosco the dog", house));
        animals.add(new Animal("Ralph the hamster", house));
        animals.add(new Animal("Molly the cat",house));
        System.out.println("animals: " + animals);
        ByteArrayOutputStream buf1 = new ByteArrayOutputStream();
        ObjectOutputStream o1 = new ObjectOutputStream(buf1);
        o1.writeObject(animals);
        o1.writeObject(animals);
        // 寫入到另外一個流中:
        ByteArrayOutputStream buf2 = new ByteArrayOutputStream();
        ObjectOutputStream o2 = new ObjectOutputStream(buf2);
        o2.writeObject(animals);
        // 反序列化:
        ObjectInputStream in1 = new ObjectInputStream(new ByteArrayInputStream(buf1.toByteArray()));
        ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(buf2.toByteArray()));
        List animals1 = (List)in1.readObject(),animals2 = (List)in1.readObject(),animals3 = (List)in2.readObject();
        System.out.println("animals1: " + animals1);
        System.out.println("animals2: " + animals2);
        System.out.println("animals3: " + animals3);
    }
    
}

class House implements Serializable{}

class Animal implements Serializable{
    private String name;
    private House preferredHouse;
    Animal(String nm, House h){
        name = nm;
        preferredHouse = h;
    }
    public String toString(){
        return name + "[" + super.toString() + "]. " + preferredHouse + "\n";
    }
}

  輸出結果:

animals: [Bosco the dog[testDemos.Animal@7852e922]. testDemos.House@4e25154f
, Ralph the hamster[testDemos.Animal@70dea4e]. testDemos.House@4e25154f
, Molly the cat[testDemos.Animal@5c647e05]. testDemos.House@4e25154f
]
animals1: [Bosco the dog[testDemos.Animal@2d98a335]. testDemos.House@16b98e56
, Ralph the hamster[testDemos.Animal@7ef20235]. testDemos.House@16b98e56
, Molly the cat[testDemos.Animal@27d6c5e0]. testDemos.House@16b98e56
]
animals2: [Bosco the dog[testDemos.Animal@2d98a335]. testDemos.House@16b98e56
, Ralph the hamster[testDemos.Animal@7ef20235]. testDemos.House@16b98e56
, Molly the cat[testDemos.Animal@27d6c5e0]. testDemos.House@16b98e56
]
animals3: [Bosco the dog[testDemos.Animal@4f3f5b24]. testDemos.House@15aeb7ab
, Ralph the hamster[testDemos.Animal@7b23ec81]. testDemos.House@15aeb7ab
, Molly the cat[testDemos.Animal@6acbcfc0]. testDemos.House@15aeb7ab
]

  這裏咱們經過一個字節數組來使用對象序列化,這樣能夠實現對任何可Serializable對象的「深度複製」(deep copy)--深度複製意味着複製的是整個對象網,而不只僅是基本對象及其引用。

  在這個例子中,咱們從打印的結果能夠看出,只要將任何對象序列化到單一流中,就能夠恢復出與咱們寫入時同樣的對象網,而且不會有任何意外重複複製出的對象,對比animals1和animals2中的House。

  另外一方面,在恢復animals3時,輸出的House與animals1和animals2是不一樣的,這說明了若是將對象序列化到不一樣的文件中,而後在代碼的不一樣部分對它們進行反序列化還原,這時會產生出兩個對象。

6. 總結

  序列化的出現給保存程序運行狀態提供了一種新的途徑,實際主要使用在RPC框架的數據傳輸以及對象狀態的持久化保存等場景。

  • 要將對象進行序列化處理,只須要實現Serializable接口,而後經過ObjectOutputStream的writeObject()方法便可完成對象的序列化;
  • 在某個類實現了Serializable接口以後,爲了保證可以成功反序列化,一般建議再添加一個序列化版本號serialVersionUID,並指定值;
  • 實現Serializable接口只能使用Java提供的默認序列化機制(即將對象全部部分序列化),若想自定義序列化過程,有以下三種方式:
  1. 實現Externalizable接口,並實現writeExternal()和readExternal()方法;
  2. 用transient修飾不但願被序列化的成員;
  3. 在類中添加名爲writeObject()和readObject()的方法,在其中指定本身的邏輯;
  • 實現Externalizable接口以後,沒有任何成員能夠自動序列化,須要在writeExternal()內部只對所需部分進行顯式的序列化,而且在readExternal()方法中將其恢復;
  • 在對實現了Serializable接口的類進行反序列化的過程當中不會調用任何構造函數,而對實現了Externalizable接口的類進行反序列化時會調用其默認構造函數,若是沒有默認構造函數,則會報java.io.InvalidClassException錯誤;
相關文章
相關標籤/搜索