1. 程式人生 > >一文看懂Java序列化

一文看懂Java序列化

    • 一文看懂Java序列化
      • 簡介
      • Java實現
      • Serializable
        • 最基本情況
        • 類的成員為引用
        • 同一物件多次序列化
        • 子父類引用序列化
        • 可自定義的可序列化
      • Externalizable:強制自定義序列化
      • 序列化版本號serialVersionUID

一文看懂Java序列化

簡介

首先我們看一下wiki上面對於序列化的解釋。

序列化(serialization)在電腦科學的資料處理中,是指將資料結構或物件狀態轉換成可取用格式(例如存成檔案,存於緩衝,或經由網路中傳送),以留待後續在相同或另一臺計算機環境中,能恢復原先狀態的過程。依照序列化格式重新獲取位元組的結果時,可以利用它來產生與原始物件相同語義的副本。對於許多物件,像是使用大量引用的複雜物件,這種序列化重建的過程並不容易。面向物件中的物件序列化,並不概括之前原始物件所關係的函式。這種過程也稱為物件編組(marshalling)。從一系列位元組提取資料結構的反向操作,是反序列化(也稱為解編組、deserialization、unmarshalling)。

以最簡單的方式來說,序列化就是將記憶體中的物件變成網路或則磁碟中的檔案。而反序列化就是將檔案變成記憶體中的物件。(emm,序列化就是將腦海中的“老婆”變成紙片人?反序列化就是將紙片人變成腦海中的“老婆”?當我沒說)如果說的程式碼中具體一點,序列化就是將物件變成位元組,而反序列化就是將位元組恢復成物件。

當然,你在一個平臺進行序列化,在另外一個平臺也可以進行反序列化。

物件的序列化主要有兩種用途:
  1. 把物件的位元組序列永久地儲存到硬碟上,通常存放在一個檔案中;(比如說伺服器上使用者的session物件)
  2. 在網路上傳送物件的位元組序列。(比如說進行網路通訊,訊息(可以是檔案)肯定要變成二進位制序列才能在網路上面進行傳輸)

OK,既然我們已經瞭解到什麼是(反)序列化了,那麼多說無益,讓我們來好好的看一看Java是怎麼實現的吧。

Java實現

對於Java這把輕機槍來說,既然序列化是一個很重要的部分,那麼它肯定自身提供了序列化的方案。

在Java中,只有實現了Serializable和Externalizable介面的類的物件才能夠進行序列化。在下面將分別對兩者進行介紹。

Serializable

最基本情況

Serializable可以說是最簡單的序列化實現方案了。它就是一個介面,裡面沒有任何的屬性和方法。一個類通過implements Serializable標示著這個類是可序列化的。下面將舉一個簡單的例子:

public class People implements Serializable {
    private String name;
    private int age;

    public People(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

People類顯而易見,是可序列化的。那麼我們如何來實現可序列化呢?在序列化的過程中,有兩個步驟:

  1. 序列化
  • 建立一個ObjectOutputStream輸出流。
  • 呼叫ObjectOutputStream的writeObject函式輸出可序列化的物件。
public class Main {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
        People people = new People("name", 18);
        oos.writeObject(people);
    }
}

ObjectOutputStream物件中需要一個輸出流,這裡使用的是檔案輸出流(也可以是用其他輸出流,例如System.out,輸出到控制檯)。然後我們通過呼叫writeObject就可以講people物件寫入到“object.txt”了。

  1. 反序列化
    我們重新編輯People的構造方法,在裡面新增一個輸出來檢視反序列化是否會進行呼叫建構函式。
public class People implements Serializable {
    private String name;
    private int age;

    public People(String name, int age) {
        System.out.println("是否呼叫序列化?");
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

反序列化和序列化一樣,也分為2個步驟:

  • 建立一個ObjectInputStream輸入流
  • 呼叫ObjectInputStream中的readObject函式得到序列化的物件
public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        People people = (People) ois.readObject();
        System.out.println(people);
    }
}

下面是程式執行之後的控制檯的圖片。


image-20200302102728666

可以很明顯的看見,反序列化的時候,並沒有呼叫People的構造方法。反序列化的物件是由JVM自己生成的物件,而不是通過構造方法生成。

Ok,通過上面我們簡單的學會了序列化的使用,那麼,我們會有一個問題,一個物件在序列化的過程中,有哪一些屬性是可是序列化的,哪一些是不可序列化的呢?

通過檢視原始碼,我們可以知道:


image-20200302104032269

物件的類,簽名和非transient和非static變數會寫入到類中。

類的成員為引用

看到很多部落格都是這樣說的:

如果一個可序列化的類的成員不是基本型別,也不是String型別,那這個引用型別也必須是可序列化的;否則,會導致此類不能序列化。

其實這樣說不是很準確,因為即使是String型別,裡面也實現了Serializable這個介面。


image-20200302105202719

我們新建一個Man類,但是它並沒有實現Serializable方法。

public class Man{
    private String sex;

    public Man(String sex) {
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Man{" +
                "sex='" + sex + '\'' +
                '}';
    }
}

然後在People類中進行引用。

public class People implements Serializable {
    private String name;
    private int age;
    private Man man;

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", man=" + man +
                '}';
    }

    public People(String name, int age, Man man) {
        this.name = name;
        this.age = age;
        this.man = man;
    }
}

如果我們進行序列化,會發生以下錯誤:

java.io.NotSerializableException: People
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at Main.main(Main.java:41)

因為Man是不可序列化的,也就導致了People類是不可序列化的。

同一物件多次序列化

大家看一下下面的這段程式碼:

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

        People people = new People("name", 11);
        oos.writeObject(people);
        oos.writeObject(people);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        People people1 = (People) ois.readObject();
        People people2 = (People) ois.readObject();
        System.out.println(people1 == people2);
    }
}

你們覺得會輸出啥?

最後的結果會輸出true

然後大家再看一段程式碼,與上面程式碼不同的是,People在第二次writeObject的時候,對name進行了重新賦值操作。

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

        People people = new People("name", 11);
        oos.writeObject(people);
        people.setName("hello");
        oos.writeObject(people);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        People people1 = (People) ois.readObject();
        People people2 = (People) ois.readObject();
        System.out.println(people1 == people2);
    }
}

結果會輸出啥?

結果還是:true,同時在people1和people2物件中,name都為“name”,而不是為“hello”。


why??為什麼會這樣?

在預設情況下,對於一個例項的多個引用,為了節省空間,只會寫入一次。而當寫入多次時,只會在後面追加幾個位元組而已(代表某個例項的引用)。

但是我們如果向在後面追加例項而不是引用那麼我們應該怎麼做?使用rest或writeUnshared即可。

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

        People people = new People("name", 11);
        oos.writeObject(people);
        people.setName("hello");
        oos.reset();
        oos.writeObject(people);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        People people1 = (People) ois.readObject();
        People people2 = (People) ois.readObject();
        System.out.println(people1);
        System.out.println(people2);
        System.out.println(people1 == people2);
    }
}
public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

        People people = new People("name", 11);
        oos.writeObject(people);
        people.setName("hello");
        oos.writeUnshared(people);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        People people1 = (People) ois.readObject();
        People people2 = (People) ois.readObject();
        System.out.println(people1);
        System.out.println(people2);
        System.out.println(people1 == people2);
    }
}

子父類引用序列化

子類和父類有兩種情況:

  • 子類沒有序列化,父類進行了序列化
  • 子類進行序列化,父類沒有進行序列化

emm,第一種情況不需要考慮,肯定不會出錯。讓我們來看一看第二種情況會怎麼樣!!

父類Man類

public class Man {
    private String sex;

    public Man(String sex) {
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Man{" +
                "sex='" + sex + '\'' +
                '}';
    }
}

子類People類:

public class People extends Man implements Serializable {

    private String name;
    private int age;

    public People(String name, int age, String sex) {
        super(sex);
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                "} " + super.toString();
    }
}

如果這個時候,我們對People進行序列化會怎麼樣呢?會報錯!!

Exception in thread "main" java.io.InvalidClassException: People; no valid constructor
    at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169)
    at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2098)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1625)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:465)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:423)
    at Main.main(Main.java:38)

如何解決,我們可以在Man中,新增一個無參構造器即可。這是因為當父類不可序列化的時候,需要呼叫預設無參構造器初始化屬性的值。

可自定義的可序列化

我們會有一個疑問,序列化可以將物件儲存在磁碟或者網路中,but,我們如何能夠保證這個序列化的檔案的不會被被人檢視到裡面的內容。假如我們在進行序列化的時候就像這些屬性進行加密不就Ok了嗎?(這個僅僅是舉一個例子)

可自定義的可序列化有兩種情況:

  • 某些變數不進行序列化
  • 在序列化的時候改變某些變數

在上面我們知道transient和static的變數不會進行序列化,因此我們可以使用transient來標記某一個變數來限制它的序列化。

在第二中情況我們可以通過重寫writeObject與readObject方法來選擇對屬性的操作。(還有writeReplace和readResolve)

在下面的程式碼中,通過transient來限制name寫入,通過writeObject和readObject來對寫入的age進行修改。

public class People implements Serializable {

    transient private String name;
    private int age;

    public People(String name, int age) {
        this.name = name;
        this.age = age;
    }
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeInt(age + 1);
    }

    private void readObject(ObjectInputStream in) throws IOException {
        this.age = in.readInt() -1 ;
    }
}

至於main函式怎麼呼叫?還是正常的呼叫:

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
        People people = new People("name", 11);
        oos.writeObject(people);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        People people1 = (People) ois.readObject();
    }
}

Externalizable:強制自定義序列化

這個,emm,“強制”兩個字都懂吧。讓我們來看一看這個介面的原始碼:

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

簡單點來說,就是類通過implements這個介面,實現這兩個方法來進行序列化的自定義。

public class People implements Externalizable {

    private String name;
    private int age;

    public People(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 注意必須要一個預設的構造方法
    public People() {
    }


    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(this.age+1);
    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.age  = in.readInt() - 1;
    }
    
}

兩者之間的差異

方案 實現Serializable介面 實現Externalizable介面
方式 系統預設決定儲存資訊 程式設計師決定儲存哪些資訊
方法 使用簡單,implements即可 必須實現介面內的兩個方法
效能 效能略差 效能略好

序列化版本號serialVersionUID

我相信很多人都看到過serialVersionUID,隨便開啟一個類(這裡是String類),我麼可以看到:

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

使用來自JDK 1.0.2 的serialVersionUID用來保持連貫性

這個serialVersionUID的作用很簡單,就是代表一個版本。當進行反序列化的時候,如果class的版本號與序列化的時候不同,則會出現InvalidClassException異常。

版本好可以只有指定,但是有一個點要值得注意,JVM會根據類的資訊自動算出一個版本號,如果你更改了類(比如說新增/修改了屬性或者方法),則計算出來的版本號就發生了改變。這樣也就代表這你無法反序列化你以前的東西。

什麼情況下需要修改serialVersionUID呢?分三種情況。

  • 修改了方法,這個當然版本好不需要改變
  • 修改了靜態變數或者transient關鍵之修飾的變數,同樣不需要修改。
  • 新增了變數或者刪除了變數也不需要修改。如果是新增了變數,則進行反序列化的時候會給新增的變數賦一個預設值。如果是修改了變數,則進行反序列化的時候無需理會被刪除的值。

講完了講完了,序列化實際上還是挺簡單。不過需要注意使用的時候遇到的坑。~~