Java 對象序列化

對象序列化

對象序列化機制容許把內存中的Java對象轉換成與平臺無關的二進制流,從而能夠保存到磁盤或者進行網絡傳輸,其它程序得到這個二進制流後能夠將其恢復成原來的Java對象。 序列化機制可使對象能夠脫離程序的運行而對立存在java

序列化的含義和意義

序列化

序列化機制可使對象能夠脫離程序的運行而對立存在程序員

序列化(Serialize)指將一個java對象寫入IO流中,與此對應的是,對象的反序列化(Deserialize)則指從IO流中恢復該java對象算法

若是須要讓某個對象能夠支持序列化機制,必須讓它的類是可序列化(serializable),爲了讓某個類可序列化的,必須實現以下兩個接口之一:數組

  • Serializable:標記接口,實現該接口無須實現任何方法,只是代表該類的實例是可序列化的網絡

  • Externalizable性能

全部在網絡上傳輸的對象都應該是可序列化的,不然將會出現異常;全部須要保存到磁盤裏的對象的類都必須可序列化;程序建立的每一個JavaBean類都實現Serializable;this

使用對象流實現序列化

實現Serializable實現序列化的類,程序能夠經過以下兩個步驟來序列化該對象:設計

1.建立一個ObjectOutputStream,這個輸出流是一個處理流,因此必須創建在其餘節點流的基礎之上code

// 建立個ObjectOutputStream輸出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

2.調用ObjectOutputStream對象的writeObject方法輸出可序列化對象對象

// 將一個Person對象輸出到輸出流中
oos.writeObject(per);

定義一個NbaPlayer類,實現Serializable接口,該接口標識該類的對象是可序列化的

public class NbaPlayer implements java.io.Serializable
{
    private String name;
    private int number;
    // 注意此處沒有提供無參數的構造器!
    public NbaPlayer(String name, int number)
    {
        System.out.println("有參數的構造器");
        this.name = name;
        this.number = number;
    }

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // number的setter和getter方法
    public void setNumber(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }
}

使用ObjectOutputStream將一個NbaPlayer對象寫入磁盤文件

import java.io.*;

public class WriteObject
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("object.txt")))
        {
            NbaPlayer player = new NbaPlayer("維斯布魯克", 0);
            // 將player對象寫入輸出流
            oos.writeObject(player);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

反序列化

從二進制流中恢復Java對象,則須要使用反序列化,程序能夠經過以下兩個步驟來序列化該對象:

1.建立一個ObjectInputStream輸入流,這個輸入流是一個處理流,因此必須創建在其餘節點流的基礎之上

// 建立個ObjectInputStream輸入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));

2.調用ObjectInputStream對象的readObject()方法讀取流中的對象,該方法返回一個Object類型的Java對象,可進行強制類型轉換成其真實的類型

// 從輸入流中讀取一個Java對象,並將其強制類型轉換爲Person類
Person p = (Person)ois.readObject();

從object.txt文件中讀取NbaPlayer對象的步驟

import java.io.*;
public class ReadObject
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("object.txt")))
        {
            // 從輸入流中讀取一個Java對象,並將其強制類型轉換爲NbaPlayer類
            NbaPlayer player = (NbaPlayer)ois.readObject();
            System.out.println("名字爲:" + player.getName()
                + "\n號碼爲:" + player.getNumber());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

反序列化讀取的僅僅是Java對象的數據,而不是Java類,所以採用反序列化恢復Java對象時,必須提供Java對象所屬的class文件,不然會引起ClassNotFoundException異常;反序列化機制無須經過構造器來初始化Java對象

若是使用序列化機制向文件中寫入了多個Java對象,使用反序列化機制恢復對象必須按照實際寫入的順序讀取。當一個可序列化類有多個父類時(包括直接父類和間接父類),這些父類要麼有無參的構造器,要麼也是可序列化的—不然反序列化將拋出InvalidClassException異常。若是父類是不可序列化的,只是帶有無參數的構造器,則該父類定義的Field值不會被序列化到二進制流中

對象引用的序列化

若是某個類的Field類型不是基本類型或者String類型,而是另外一個引用類型,那麼這個引用類型必須是可序列化的,不然有用該類型的Field的類也是不可序列化的

public class AllStar implements java.io.Serializable
{
    private String name;
    private NbaPlayer player;
    public AllStar(String name, NbaPlayer player)
    {
        this.name = name;
        this.player = player;
    }

    // name的setter和getter方法
    public String getName()
    {
        return this.name;
    }

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

    // player的setter和getter方法
    public NbaPlayer getPlayer() 
    {
        return player;
    }
    
    public void setPlayer(NbaPlayer player) 
    {
        this.player = player;
    }
}

Java特殊的序列化算法

  • 全部保存到磁盤中的對象都有一個序列化編號

  • 當程序試圖序列化一個對象時,程序將先檢查該對象是否已經被序列化過,只有該對象從未(在本次虛擬中機)被序列化過,系統纔會將該對象轉換成字節序列並輸出

  • 若是某個對象已經序列化過,程序將只是直接輸出一個序列化編號,而不是再次從新序列化該對象

import java.io.*;
public class WriteAllStar
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("allStar.txt")))
        {
            NbaPlayer player = new NbaPlayer("詹姆斯哈登", 13);
            AllStar allStar1 = new AllStar("西部全明星", player);
            AllStar allStar2 = new AllStar("首發後衛", player);
            // 依次將四個對象寫入輸出流
            oos.writeObject(allStar1);
            oos.writeObject(allStar2);
            oos.writeObject(player);
            oos.writeObject(allStar2);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

4個寫入輸出流的對象,實際上只序列化了3個,並且序列的兩個AllStar對象的player引用實際是同一個NbaPlayer對象。如下程序讀取序列化文件中的對象

import java.io.*;
public class ReadAllStar
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectInputStream輸出流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("allStar.txt")))
        {
            // 依次讀取ObjectInputStream輸入流中的四個對象
            AllStar star1 = (AllStar)ois.readObject();
            AllStar star2 = (AllStar)ois.readObject();
            NbaPlayer player = (NbaPlayer)ois.readObject();
            AllStar star3 = (AllStar)ois.readObject();
            // 輸出true
            System.out.println("star1的player引用和player是否相同:"
                + (star1.getPlayer() == player));
            // 輸出true
            System.out.println("star2的player引用和player是否相同:"
                + (star2.getPlayer() == player));
            // 輸出true
            System.out.println("star2和star3是不是同一個對象:"
                + (star2 == star3));
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

若是屢次序列化同一個可變Java對象時,只有第一次序列化時纔會把該Java對象轉換成字節序列並輸出

當使用Java序列化機制序列化可變對象時,只有第一次調用WriteObject()方法來輸出對象時纔會將對象轉換成字節序列,並寫入到ObjectOutputStream;即便在後面程序中,該對象的實例變量發生了改變,再次調用WriteObject()方法輸出該對象時,改變後的實例變量也不會被輸出

import java.io.*;

public class SerializeMutable
{
    public static void main(String[] args)
    {

        try(
            // 建立一個ObjectOutputStream輸入流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("mutable.txt"));
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("mutable.txt")))
        {
            NbaPlayer player = new NbaPlayer("斯蒂芬庫裏", 30);
            // 系統會player對象轉換字節序列並輸出
            oos.writeObject(player);
            // 改變per對象的name實例變量
            player.setName("塞斯庫裏");
            // 系統只是輸出序列化編號,因此改變後的name不會被序列化
            oos.writeObject(player);
            NbaPlayer player1 = (NbaPlayer)ois.readObject();    //①
            NbaPlayer player2 = (NbaPlayer)ois.readObject();    //②
            // 下面輸出true,即反序列化後player1等於player2
            System.out.println(player1 == player2);
            // 下面依然看到輸出"斯蒂芬庫裏",即改變後的實例變量沒有被序列化
            System.out.println(player2.getName());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

自定義序列化

在一些特殊的場景下,若是一個類裏包含的某些實例變量是敏感信息,這時不但願系統將該實例變量值進行實例化;或者某個實例變量的類型是不可序列化的,所以不但願對該實例變量進行遞歸實例化,以免引起java.io.NotSerializableException異常

當對某個對象進行序列化時,系統會自動把該對象的全部實例變量依次進行序列化,若是某個實例變量引用到另外一個對象,則被引用的對象也會被序列化;若是被引用的對象的實例變量也引用了其餘對象,則被引用的對象也會被序列化,這種狀況被稱爲遞歸序列化

在實例變量前面使用transient關鍵字修飾,能夠指定java序列化時無須理會該實例變量

public class NbaPlayer implements java.io.Serializable
{
    private String name;
    private transient int number;
    // 注意此處沒有提供無參數的構造器!
    public NbaPlayer(String name, int number)
    {
        System.out.println("有參數的構造器");
        this.name = name;
        this.number = number;
    }
    
    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // number的setter和getter方法
    public void setNumber(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }
}

transient關鍵字只能用於修飾實例變量,不可修飾Java程序中的其餘成分

import java.io.*;

public class TransientTest
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("transient.txt"));
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("transient.txt")))
        {
            NbaPlayer per = new NbaPlayer("克萊湯普森", 11);
            // 系統會per對象轉換字節序列並輸出
            oos.writeObject(per);
            NbaPlayer p = (NbaPlayer)ois.readObject();
            System.out.println(p.getNumber());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

在序列化和反序列化過程當中須要特殊處理的類應該提供以下特殊簽名的方法,這些特殊的方法用以實現自定義:

  • private void writeObject(java.io.ObjectOutputStream out) throws IOException

  • private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException

  • private void readObjectNoData() throws ObjectStreamException

writeObject()方法負責寫入特定類的實例的狀態,以便相應的readObject()方法能夠恢復它。經過重寫該方法,能夠徹底得到對序列化機制的控制,自主決定哪些實例變量須要序列化,怎樣序列化。在默認狀況下,該方法會調用out.defaultWriteObject來保存Java對象的各實例變量,從而能夠實現序列化Java對象狀態的目的

readObject()方法負責從流中讀取並恢復對象實例變量,經過重寫該方法,能夠徹底得到對反序列化機制的控制,能夠自主決定須要反序列化哪些實例變量,怎樣反序列化。在默認狀況下,該方法會調用in.defaultReadObject來恢復Java對象的非瞬態實例變量

一般狀況下readObject()方法與writeObject()方法對應,若是writeObject()方法中對Java對象的實例變量進行了一些處理,則應該在readObject()方法中對該實例變量進行相應的反處理,以便正確恢復該對象

當序列化流不完整時,readObjectNoData()方法能夠用來正確地初始化反序列化的對象

import java.io.IOException;

public class NbaPlayer implements java.io.Serializable
{
    private String name;
    private int number;
    // 注意此處沒有提供無參數的構造器!
    public NbaPlayer(String name, int number)
    {
        System.out.println("有參數的構造器");
        this.name = name;
        this.number = number;
    }
    
    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // number的setter和getter方法
    public void setNumber(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }
    
    private void writeObject(java.io.ObjectOutputStream out) throws IOException
    {
        // 將name實例變量值反轉後寫入二進制流
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(number);
    }
    
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
    {
        // 將讀取的字符串反轉後賦給name實例變量
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.number = in.readInt();
    }
}

writeObject()方法存儲實例變量的順序應該和readObject()方法中恢復實例變量的順序一致,不然將不能正常恢復該Java對象

ANY-ACCESS-MODIFIER Object writeReplace() 實現序列化某個對象時替換該對象

此writeReplace()方法將由序列化機制調用,只要該方法存在。由於該方法能夠擁有私有(private),受保護的(protected)和包私有(package-private)等訪問權限,因此其子類有可能得到該方法

下面程序的writeReplace()方法,這樣能夠在寫入NbaPlayer對象時將該對象替換成ArrayList

// 重寫writeReplace方法,程序在序列化該對象以前,先調用該方法
private Object writeReplace() throws ObjectStreamException
{
    ArrayList<Object> list = new ArrayList<>();
    list.add(name);
    list.add(age);
    return list;
}

Java的序列化機制保證在序列化某個對象以前,先調用該對象的writeReplace()方法,若是該方法返回另外一個Java對象,則系統轉爲序列化另外一個對象。以下程序表面上是序列化NbaPlayer對象,但實際上序列化的是ArrayList

// 系統將player對象轉換字節序列並輸出
oos.writeObject(player);
// 反序列化讀取獲得的是ArrayList
ArrayList list = (ArrayList)ois.readObject();
System.out.println(list);

系統在序列化某個對象以前,會先調用該對象的writeReplace()和writeObject()兩個方法,系統老是先調用被序列化對象的writeReplace()方法,若是該方法返回另外一個對象,系統將再次調用另外一個對象的writeReplace()方法,直到該方法再也不返回另外一個對象爲止,程序最後將調用該對象的writeObject()方法來保存該對象的狀態

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException實現保護性複製整個對象,緊挨着readObject()以後被調用,該方法的返回值將會代替原來反序列化的對象,而原來readObject()反序列化的對象將會當即丟棄

Java的序列化機制:首先調用writeReplace(),其次調用writeObject(),最後調用writeResolve()

readObject()方法在序列化單例類,枚舉類時尤爲有用

反序列化機制在恢復java對象時無須調用構造器來初始化java對象。從這個意義上來看,序列化機制能夠用來"克隆"對象;全部單例類,枚舉類在實現序列化時都應該提供readResolve()方法,這樣才能夠保證反序列化的對象依然正常;readResolve()方法建議使用final修飾

另外一種自定義序列化機制

這種序列化方式徹底由程序員決定存儲和恢復對象數據。要實現該目標,必須實現Externalizable接口,該接口裏定義了以下兩個方法:

  • void readExternal(ObjectInput in):須要序列化的類實現readExternal()方法來實現反序列化。該方法調用DataInput(它是ObjectInput的父接口)的方法來恢復基本類型的實例變量值,調用ObjectInput的readObject()方法來恢復引用類型的實例變量值

  • void writeExternal(Object out):須要序列化的類實現該方法來保存對象的狀態。該方法調用DataOutput(它是ObjectOutput的父接口)的方法來保存基本類型的實例變量值,調用ObjectOutput的writeObject()方法來保存引用類型的實例變量值

import java.io.*;

public class Player
    implements java.io.Externalizable
{
    private String name;
    private int number;
    // 注意此處沒有提供無參數的構造器!
    public Player(String name, int number)
    {
        System.out.println("有參數的構造器");
        this.name = name;
        this.number = number;
    }
    // 省略name與number的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // number的setter和getter方法
    public void setNumber(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }

    public void writeExternal(java.io.ObjectOutput out)
        throws IOException
    {
        // 將name實例變量的值反轉後寫入二進制流
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(number);
    }
    public void readExternal(java.io.ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        // 將讀取的字符串反轉後賦給name實例變量
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.number = in.readInt();
    }
}

兩種序列化機制的對比

實現Serializable接口 實現Externalizable接口
系統自動存儲必要信息 程序員決定存儲哪些信息
Java內建支持,易於實現,只需實現該接口便可,無須任何代碼支持 僅僅提供兩個空方法,實現該接口必須爲兩個空方法提供實現
性能略差 性能略好

對象序列化的注意事項:

  • 對象的類名、實例變量(包括基本類型、數組、對其餘對象的引用)都會被序列化;方法、類變量(即static修飾的成員變量)、transient實例變量(也被稱爲瞬態實例變量)都不會被序列化

  • 反序列化讀取的僅僅是Java對象的數據,而不是Java類,所以採用反序列化恢復Java對象時,必須提供Java對象所屬的class文件,不然會引起ClassNotFoundException異常

  • 實現Serializable接口的類若是須要讓某個實例變量不被序列化,則能夠在該實例變量前加transient修飾符,而不是static關鍵字,雖然static關鍵字也能夠達到這個效果,但static關鍵字不能這樣用

  • 保證序列化對象的實例變量類型也是可序列化的,不然須要使用transient修飾該變量

  • 當經過文件、網絡來讀取序列化後的對象時,必須按照實際寫入的順序讀取

版本

隨着項目的設計,系統的class文件也會升級,Java如何保證兩個class文件的兼容性?爲了在反序列化時確保序列化版本的兼容性,最好在每一個要序列化的類中加入private static final long serialVersionUID這個屬性,具體數值自定義。這樣,即便某個類在與之對應的對象已經序列化出去後作了修改,該對象依然能夠被正確反序列化

如不顯式定義該變量值,這個變量值將由JVM根據類的相關信息計算,而修改後的類的計算結果與修改前的類的計算結果每每不一樣,從而形成對象的反序列化由於類版本不兼容而失敗

致使該類實例的反序列化失敗的類修改操做:

  • 若是修改類時僅僅修改了方法,則反序列化徹底不受任何影響,類定義無需修改serizlVersionUID屬性值

  • 若是修飾類時僅僅修改了靜態屬性或瞬態(transient)屬性,則反序列化不受任何影響,類定義無需修改serialVersionUID屬性值

  • 若是修改類時修飾了非靜態、非瞬態屬性,則可能致使序列化版本不兼容,若是對象流中的對象和新類中包含同名的屬性,而屬性類型不一樣,則反序列化失敗 ,類定義應該更新serialVersionUID屬性值。若是新類比對象流中對象包含更多的 屬性,序列化版本也能夠兼容,類定義能夠不更新serialVersionUID屬性值;但反序列化獲得的新對象中多出的屬性值都是null(引用類型屬性)或0(基本類型屬性)

相關文章
相關標籤/搜索