天天看點

你了解java序列化嗎?一篇文章搞懂java序列化

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

序列化(serialization)在計算機科學的資料進行中,是指将資料結構或對象狀态轉換成可取用格式(例如存成檔案,存于緩沖,或經由網絡中發送),以留待後續在相同或另一台計算機環境中,能恢複原先狀态的過程。依照序列化格式重新擷取位元組的結果時,可以利用它來産生與原始對象相同語義的副本。對于許多對象,像是使用大量引用的複雜對象,這種序列化重建的過程并不容易。面向對象中的對象序列化,并不概括之前原始對象所關系的函數。這種過程也稱為對象編組(marshalling)。從一系列位元組提取資料結構的反向操作,是反序列化(也稱為解編組、deserialization、unmarshalling)。

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

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

對象的序列化主要有兩種用途:

1. 把對象的位元組序列永久地儲存到硬碟上,通常存放在一個檔案中;(比如說伺服器上使用者的session對象)

2. 在網絡上傳送對象的位元組序列。(比如說進行網絡通信,消息(可以是檔案)肯定要變成二進制序列才能在網絡上面進行傳輸)

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

Java實作

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

在Java中,隻有實作了Serializable和Externalizable接口的類的對象才能夠進行序列化。在下面将分别對兩者進行介紹。

Serializable

最基本情況

Serializable可以說是最簡單的序列化實作方案了。它就是一個接口,裡面沒有任何的屬性和方法。一個類通過implements Serializable标示着這個類是可序列化的。下面将舉一個簡單的例子:

public classPeople implementsSerializable {
    private String name;
    private int age;

    publicPeople(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 classMain {
    publicstaticvoidmain(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 classPeople implementsSerializable {
    private String name;
    private int age;

    publicPeople(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 classMain {
    publicstaticvoidmain(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        People people = (People) ois.readObject();
        System.out.println(people);
    }
}      

下面是程式運作之後的控制台的圖檔。

你了解java序列化嗎?一篇文章搞懂java序列化

可以很明顯的看見,反序列化的時候,并沒有調用People的構造方法。反序列化的對象是由JVM自己生成的對象,而不是通過構造方法生成。

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

通過檢視源代碼,我們可以知道:

你了解java序列化嗎?一篇文章搞懂java序列化

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

類的成員為引用

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

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

其實這樣說不是很準确,因為即使是String類型,裡面也實作了Serializable這個接口。

你了解java序列化嗎?一篇文章搞懂java序列化

我們建立一個Man類,但是它并沒有實作Serializable方法。

public classMan{
    private String sex;

    publicMan(String sex) {
        this.sex = sex;
    }

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

然後在People類中進行引用。

public classPeople implementsSerializable {
    private String name;
    private int age;
    private Man man;

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

    publicPeople(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 classMain {
    publicstaticvoidmain(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 classMain {
    publicstaticvoidmain(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”。

你了解java序列化嗎?一篇文章搞懂java序列化

 why??為什麼會這樣?

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

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

public classMain {
    publicstaticvoidmain(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 classMain {
    publicstaticvoidmain(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 classMan {
    private String sex;

    publicMan(String sex) {
        this.sex = sex;
    }

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

子類People類:

public classPeople extendsMan implementsSerializable {

    private String name;
    private int age;

    publicPeople(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 classPeople implementsSerializable {

    transient private String name;
    private int age;

    publicPeople(String name, int age) {
        this.name = name;
        this.age = age;
    }
    privatevoidwriteObject(ObjectOutputStream out) throws IOException {
        out.writeInt(age + 1);
    }

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

至于main函數怎麼調用?還是正常的調用:

public classMain {
    publicstaticvoidmain(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 interfaceExternalizable extendsjava.io.Serializable {
    voidwriteExternal(ObjectOutput out) throws IOException;
    voidreadExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}      

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

public classPeople implementsExternalizable {

    private String name;
    private int age;

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


    publicvoidwriteExternal(ObjectOutput out) throws IOException {
        out.writeInt(this.age+1);
    }

    publicvoidreadExternal(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關鍵之修飾的變量,同樣不需要修改。
  • 新增了變量或者删除了變量也不需要修改。如果是新增了變量,則進行反序列化的時候會給新增的變量賦一個預設值。如果是修改了變量,則進行反序列化的時候無需理會被删除的值。