java序列化

Acelin_H發表於2021-08-04



什麼是序列化?


一句話概括。序列化將物件的狀態資訊轉換為可以儲存或傳輸的形式的過程。而Java序列化,就是指把Java物件轉換為“有序”位元組序列的過程。相反的,把“有序”位元組序列恢復為Java物件的過程稱之為反序列化

怎麼理解上面的描述呢?從序列化的定義的可以看出,序列化其實是一個用來保障儲存和傳輸的機制,而其針對的物件,肯定就是那些不便儲存和傳輸的資訊,而這體現在java程式設計中,就是類的例項 —— 類物件。

上面提到的“有序”,它並不是真的有順序,其實是比較抽象化的表達,以此來表達經過序列化處理的物件,能夠通過反序列化恢復成原來的樣子這樣一個過程

舉個例子,你要搬家了,家裡有個大鞋架,因為鞋架太大了,不便儲存和運輸,於是你把鞋機拆散了,然後把拆解步驟記錄下來,運輸到新家後,你再根據步驟記錄把零件組裝成鞋架原來的樣子。拆鞋架和組裝鞋架其實就是序列化和反序列的過程,而你記錄的那份拆解步驟,就是序列化協議。


怎麼實現序列化?



一、實現Serializable介面

定義一個類實現Serializable介面:

public class Rectangle implements Serializable {

    private int width;
    private int length;

    public Rectangle(){}

    public Rectangle(int width,int length){
        this.width = width;
        this.length = length;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    @Override
    public String toString() {
        return "Rectangle{" +
                "width=" + width +
                ", length=" + length +
                '}';
    }

}

二、實現Externalizable介面

實現writeExternalreadExternal方法

public class Rectangle implements Externalizable {

    private int width;
    private int length;

    public Rectangle(){}

    public Rectangle(int width,int length){
        this.width = width;
        this.length = length;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    @Override
    public String toString() {
        return "Rectangle{" +
                "width=" + width +
                ", length=" + length +
                '}';
    }

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

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

    }
}

序列化和反序列化

public static void main(String[] args){

    /* 序列化 */
    Rectangle rectangle = new Rectangle(6,8);
    try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rectangle.out"))){
        oos.writeObject(rectangle);
    }catch(Exception ex){
        ex.printStackTrace();
    }

    /* 反序列化 */
    try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("rectangle.out"))){
        Rectangle rectangle1 = (Rectangle) ois.readObject();
        System.out.println(rectangle1);
    }catch(Exception ex){
        ex.printStackTrace();
    }

}

正常的輸出結果:

Rectangle{width=6, length=8}

當實現Externalizable介面,沒有對writeExternalreadExternal重寫時,結果如下

Rectangle{width=0, length=0}

也就是說的,實現Externalizable介面,需要自己實現序列化和反序列邏輯,程式設計人員用比較高的靈活度,這點在接下來的自定義序列化會講到


自定義序列化?



transient關鍵字

transient關鍵字修飾的成員的變數,序列化時將被忽略。該方法決定變數是否序列化

該自定義方法級別最低,當使用的下面的任一方法時,transient關鍵字將失效。換句話說,使用下面各方法進行序列化自定義,那麼transient修飾的變數同樣會被序列化。


自定義readObject和writeObject方法

通過的類中自定義readObjectwriteObject方法,可以控制該類的序列化和反序列化邏輯,jvm在進行序列化的時候會自動呼叫readObject方法,反序列化呼叫writeObject

private void readObject(ObjectInputStream ois) throws IOException { 
    this.length = ois.readInt() / 100;
    this.width = ois.readInt();
}

private void writeObject(ObjectOutputStream oos)throws IOException{
    this.length = this.length * 100;
    oos.writeInt(this.length);
    oos.writeInt(this.width);
}

自定義writeReplace方法

writeReplace方法沒有入參,用於改變序列化物件的型別,且會使用預設的序列化機。也就是說,上面提到的自定義readObject和writeObject方法都將失效。以下為例項:

private Object writeReplace() throws ObjectStreamException {

    List<Object> list = new ArrayList<>();
    list.add(this.length * 1000);
    list.add(this.width *1000);
    return list; 
}

相應的,在進行反序列化的時候,就應該使用新的型別進行接收,如該例子,應該用List<Object> 接收反序列化物件。


自定義readResolve方法

readResolve方法用於替換反序列化出來的物件,該方法同樣沒有入參。

private Object readResolve() throws ObjectStreamException {

    System.out.println("自定義readResolve方法被呼叫");
    return new Rectangle(9,10);
}

由於沒發拿到序列化的資訊,因此常用來控制單例模式下,反序列化出來的物件為原先的單例物件,以維護單例模式中反序列化物件的單一性。


實現Externalizable介面

這個已經在上面例項化方式中有所提及。實現該介面必須強制實現writeExternalreadExternal 兩個方法如下:

@Override
public void writeExternal(ObjectOutput out) throws IOException {
    this.length = this.length * 100;
    out.writeInt(this.length);
    out.writeInt(this.width);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    this.length = in.readInt() / 100;
    this.width = in.readInt();
}

該效果等同於上面實現Serializable介面,然後自定義readObjectwriteObjectf方法


序列化的注意事項


  1. serialVersionUID 序列化版本號

    用來區分需要序列化類的版本。假如序列化的類中發生了變數的修改,那麼該版本號一般也要跟著修改,才能夠在反序列化的時候得到對應版本的類物件,否則會丟擲InvalidClassException異常

  2. 序列化與類的初始化一樣,是的一個遞迴的過程。

    當一個類實現了序列化介面,該類的成員除了基本型別和String型別之外,其他的引用型別也必須是可序列化的;否則會拋NotSerializableException異常

  3. 同一物件多次序列化只會序列化一次

    如果程式對同一個物件有多次序列化,那麼只會在第一次進行序列化,後續實際上是返回一個編號進行區分

  4. 反序列化的順序與序列化時的順序一致,類似佇列模型


序列化的使用場景


講了這麼多,那序列化到底用在什麼地方呢?

由jvm的記憶體結構相關知識我們可以知道,java物件都被儲存在堆記憶體中。在jvm處於執行狀態的時候,我們能夠對物件的進行復用。但一旦jvm生命週期結束,相關物件也隨之被回收。也就是說的,如果希望在jvm停止後還能夠拿到某些物件,這時候就需要用到java的序列化。

因此大概由以下幾種場景會使用到java的序列化

  • 當你想把的記憶體中的物件狀態儲存到一個檔案中或者資料庫中時候;
  • 當你想用套接字在網路上傳送物件的時候;
  • 當你想通過RMI(遠端方法呼叫)傳輸物件的時候;

相關文章