一文看懂Java序列化


 

 

 

一文看懂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了嗎?(這個僅僅是舉一個例子)

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

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

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

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

在下面的代碼中,通過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關鍵之修飾的變量,同樣不需要修改。
  • 新增了變量或者刪除了變量也不需要修改。如果是新增了變量,則進行反序列化的時候會給新增的變量賦一個默認值。如果是修改了變量,則進行反序列化的時候無需理會被刪除的值。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM