一文看懂Java序列化

一文看懂Java序列化

簡介

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

序列化(serialization)在計算機科學的數據處理中,是指將數據結構或對象狀態轉換成可取用格式(例如存成文件,存於緩衝,或經由網絡中發送),以留待後續在相同或另外一臺計算機環境中,能恢復原先狀態的過程。依照序列化格式從新獲取字節的結果時,能夠利用它來產生與原始對象相同語義的副本。對於許多對象,像是使用大量引用的複雜對象,這種序列化重建的過程並不容易。面向對象中的對象序列化,並不歸納以前原始對象所關係的函數。這種過程也稱爲對象編組(marshalling)。從一系列字節提取數據結構的反向操做,是反序列化(也稱爲解編組、deserialization、unmarshalling)。java

以最簡單的方式來講,序列化就是將內存中的對象變成網絡或則磁盤中的文件。而反序列化就是將文件變成內存中的對象。(emm,序列化就是將腦海中的「老婆」變成紙片人?反序列化就是將紙片人變成腦海中的「老婆」?當我沒說)若是說的代碼中具體一點,序列化就是將對象變成字節,而反序列化就是將字節恢復成對象。程序員

固然,你在一個平臺進行序列化,在另一個平臺也能夠進行反序列化。服務器

對象的序列化主要有兩種用途:
  1. 把對象的字節序列永久地保存到硬盤上,一般存放在一個文件中;(好比說服務器上用戶的session對象)
  2. 在網絡上傳送對象的字節序列。(好比說進行網絡通訊,消息(能夠是文件)確定要變成二進制序列才能在網絡上面進行傳輸)網絡

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

Java實現

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

在Java中,只有實現了Serializable和Externalizable接口的類的對象纔可以進行序列化。在下面將分別對二者進行介紹。ide

Serializable

最基本狀況

Serializable能夠說是最簡單的序列化實現方案了。它就是一個接口,裏面沒有任何的屬性和方法。一個類經過implements Serializable標示着這個類是可序列化的。下面將舉一個簡單的例子:svg

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
image-20200302102728666

能夠很明顯的看見,反序列化的時候,並無調用People的構造方法。反序列化的對象是由JVM本身生成的對象,而不是經過構造方法生成。

Ok,經過上面咱們簡單的學會了序列化的使用,那麼,咱們會有一個問題,一個對象在序列化的過程當中,有哪一些屬性是但是序列化的,哪一些是不可序列化的呢?

經過查看源代碼,咱們能夠知道:

image-20200302104032269
image-20200302104032269

對象的類,簽名和非transient和非static變量會寫入到類中。

類的成員爲引用

看到不少博客都是這樣說的:

若是一個可序列化的類的成員不是基本類型,也不是String類型,那這個引用類型也必須是可序列化的;不然,會致使此類不能序列化。

其實這樣說不是很準確,由於即便是String類型,裏面也實現了Serializable這個接口。

image-20200302105202719
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關鍵之修飾的變量,一樣不須要修改。
  • 新增了變量或者刪除了變量也不須要修改。若是是新增了變量,則進行反序列化的時候會給新增的變量賦一個默認值。若是是修改了變量,則進行反序列化的時候無需理會被刪除的值。

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

相關文章
相關標籤/搜索