天天看點

Java 序列化Java 序列化

Java 序列化

簡介

定義

序列化:序列化是将對象轉換為位元組流。

反序列化:反序列化是将位元組流轉換為對象。

用途

序列化的用途有:

  • 序列化可以将對象的位元組序列持久化——儲存在記憶體、檔案、資料庫中。
  • 在網絡上傳送對象的位元組序列。
  • RMI(遠端方法調用)

序列化和反序列化

Java 通過對象輸入輸出流來實作序列化和反序列化:

  • 序列化:

    java.io.ObjectOutputStream

    類的

    writeObject()

    方法可以實作序列化;
  • 反序列化:

    java.io.ObjectInputStream

    readObject()

    方法用于實作反序列化。

序列化和反序列化示例:

public class SerializeDemo01 {
    enum Sex {
        MALE, FEMALE
    }

    static class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name = null;
        private Integer age = null;
        private Sex sex;

        public Person() {
            System.out.println("call Person()");
        }

        public Person(String name, Integer age, Sex sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
        }

        public String toString() {
            return "name: " + this.name + ", age: " + this.age + ", sex: " + this.sex;
        }
    }

    /**
     * 序列化
     */
    private static void serialize(String filename) throws IOException {
        File f = new File(filename); // 定義儲存路徑
        OutputStream out = new FileOutputStream(f); // 檔案輸出流
        ObjectOutputStream oos = new ObjectOutputStream(out); // 對象輸出流
        oos.writeObject(new Person("Jack", 30, Sex.MALE)); // 儲存對象
        oos.close();
        out.close();
    }

    /**
     * 反序列化
     */
    private static void deserialize(String filename) throws IOException, ClassNotFoundException {
        File f = new File(filename); // 定義儲存路徑
        InputStream in = new FileInputStream(f); // 檔案輸入流
        ObjectInputStream ois = new ObjectInputStream(in); // 對象輸入流
        Object obj = ois.readObject(); // 讀取對象
        ois.close();
        in.close();
        System.out.println(obj);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        final String filename = "d:/text.dat";
        serialize(filename);
        deserialize(filename);
    }
}           

輸出:

name: Jack, age: 30, sex: MALE           

Serializable 接口

被序列化的類必須屬于 Enum、Array 和 Serializable 類型其中的任何一種。

如果不是 Enum、Array 的類,如果需要序列化,必須實作

java.io.Serializable

接口,否則将抛出

NotSerializableException

異常。這是因為:在序列化操作過程中會對類型進行檢查,如果不滿足序列化類型要求,就會抛出異常。

我們不妨做一個小嘗試:将 SerializeDemo01 示例中 Person 類改為如下實作,然後看看運作結果。

public class UnSerializeDemo {
    static class Person { // 其他内容略 }
    // 其他内容略
}           

輸出:結果就是出現如下異常資訊。

Exception in thread "main" java.io.NotSerializableException:
...           

serialVersionUID

請注意

serialVersionUID

字段,你可以在 Java 世界的無數類中看到這個字段。

serialVersionUID 有什麼作用,如何使用 serialVersionUID?

serialVersionUID

是 Java 為每個序列化類産生的版本辨別。它可以用來保證在反序列時,發送方發送的和接受方接收的是可相容的對象。如果接收方接收的類的

serialVersionUID

與發送方發送的

serialVersionUID

不一緻,會抛出

InvalidClassException

如果可序列化類沒有顯式聲明

serialVersionUID

,則序列化運作時将基于該類的各個方面計算該類的預設

serialVersionUID

值。盡管這樣,還是建議在每一個序列化的類中顯式指定

serialVersionUID

的值。因為不同的 jdk 編譯很可能會生成不同的

serialVersionUID

預設值,進而導緻在反序列化時抛出

InvalidClassExceptions

異常。

serialVersionUID

字段必須是

static final long

類型。

我們來舉個例子:

(1)有一個可序列化類 Person

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private Integer age;
    private String address;
    // 構造方法、get、set 方法略
}           

(2)開發過程中,對 Person 做了修改,增加了一個字段 email,如下:

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private Integer age;
    private String address;
    private String email;
    // 構造方法、get、set 方法略
}           

由于這個類和老版本不相容,我們需要修改版本号:

private static final long serialVersionUID = 2L;           

再次進行反序列化,則會抛出

InvalidClassException

綜上所述,我們大概可以清楚:serialVersionUID 用于控制序列化版本是否相容。若我們認為修改的可序列化類是向後相容的,則不修改 serialVersionUID。

預設序列化機制

如果僅僅隻是讓某個類實作

Serializable

接口,而沒有其它任何處理的話,那麼就是使用預設序列化機制。

使用預設機制,在序列化對象時,不僅會序列化目前對象本身,還會對其父類的字段以及該對象引用的其它對象也進行序列化。同樣地,這些其它對象引用的另外對象也将被序列化,以此類推。是以,如果一個對象包含的成員變量是容器類對象,而這些容器所含有的元素也是容器類對象,那麼這個序列化的過程就會較複雜,開銷也較大。

注意:這裡的父類和引用對象既然要進行序列化,那麼它們當然也要滿足序列化要求:被序列化的類必須屬于 Enum、Array 和 Serializable 類型其中的任何一種。

非預設序列化機制

在現實應用中,有些時候不能使用預設序列化機制。比如,希望在序列化過程中忽略掉敏感資料,或者簡化序列化過程。下面将介紹若幹影響序列化的方法。

transient 關鍵字

當某個字段被聲明為 transient 後,預設序列化機制就會忽略該字段。

我們将 SerializeDemo01 示例中的内部類 Person 的 age 字段聲明為

transient

,如下所示:

public class SerializeDemo02 {
    static class Person implements Serializable {
        transient private Integer age = null;
        // 其他内容略
    }
    // 其他内容略
}           
name: Jack, age: null, sex: MALE           

從輸出結果可以看出,age 字段沒有被序列化。

Externalizable 接口

無論是使用 transient 關鍵字,還是使用 writeObject()和 readObject()方法,其實都是基于 Serializable 接口的序列化。

JDK 中提供了另一個序列化接口--

Externalizable

可序列化類實作

Externalizable

接口之後,基于 Serializable 接口的預設序列化機制就會失效。

我們來基于 SerializeDemo02 再次做一些改動,代碼如下:

public class ExternalizeDemo01 {
    static class Person implements Externalizable {
        transient private Integer age = null;
        // 其他内容略

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            out.writeInt(age);
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            age = in.readInt();
        }

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

        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { }
    }
     // 其他内容略
}           
call Person()
name: null, age: null, sex: null           

從該結果,一方面可以看出 Person 對象中任何一個字段都沒有被序列化。另一方面,如果細心的話,還可以發現這此次序列化過程調用了 Person 類的無參構造方法。

  • Externalizable 繼承于 Serializable,它增添了兩個方法:writeExternal() 與 readExternal()。這兩個方法在序列化和反序列化過程中會被自動調用,以便執行一些特殊操作。當使用該接口時,序列化的細節需要由程式員去完成。如上所示的代碼,由于 writeExternal() 與 readExternal() 方法未作任何處理,那麼該序列化行為将不會儲存/讀取任何一個字段。這也就是為什麼輸出結果中所有字段的值均為空。
  • 另外,若使用 Externalizable 進行序列化,當讀取對象時,會調用被序列化類的無參構造方法去建立一個新的對象;然後再将被儲存對象的字段的值分别填充到新對象中。這就是為什麼在此次序列化過程中 Person 類的無參構造方法會被調用。由于這個原因,實作 Externalizable 接口的類必須要提供一個無參的構造方法,且它的通路權限為 public。

對上述 Person 類作進一步的修改,使其能夠對 name 與 age 字段進行序列化,但要忽略掉 gender 字段,如下代碼所示:

public class ExternalizeDemo02 {
    static class Person implements Externalizable {
        transient private Integer age = null;
        // 其他内容略

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            out.writeInt(age);
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            age = in.readInt();
        }

        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeObject(name);
            out.writeInt(age);
        }

        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            name = (String) in.readObject();
            age = in.readInt();
        }
    }
     // 其他内容略
}           
call Person()
name: Jack, age: 30, sex: null           

Externalizable 接口的替代方法

實作 Externalizable 接口可以控制序列化和反序列化的細節。它有一個替代方法:實作

Serializable

接口,并添加

writeObject(ObjectOutputStream out)

readObject(ObjectInputStream in)

方法。序列化和反序列化過程中會自動回調這兩個方法。

示例如下所示:

public class SerializeDemo03 {
    static class Person implements Serializable {
        transient private Integer age = null;
        // 其他内容略

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            out.writeInt(age);
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            age = in.readInt();
        }
        // 其他内容略
    }
    // 其他内容略
}           
name: Jack, age: 30, sex: MALE           

在 writeObject()方法中會先調用 ObjectOutputStream 中的 defaultWriteObject()方法,該方法會執行預設的序列化機制,如上節所述,此時會忽略掉 age 字段。然後再調用 writeInt() 方法顯示地将 age 字段寫入到 ObjectOutputStream 中。readObject() 的作用則是針對對象的讀取,其原理與 writeObject()方法相同。

注意:writeObject()與 readObject()都是 private 方法,那麼它們是如何被調用的呢?毫無疑問,是使用反射。詳情可見 ObjectOutputStream 中的 writeSerialData 方法,以及 ObjectInputStream 中的 readSerialData 方法。

readResolve() 方法

當我們使用 Singleton 模式時,應該是期望某個類的執行個體應該是唯一的,但如果該類是可序列化的,那麼情況可能會略有不同。此時對第 2 節使用的 Person 類進行修改,使其實作 Singleton 模式,如下所示:

public class SerializeDemo04 {

    enum Sex {
        MALE, FEMALE
    }

    static class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name = null;
        transient private Integer age = null;
        private Sex sex;
        static final Person instatnce = new Person("Tom", 31, Sex.MALE);

        private Person() {
            System.out.println("call Person()");
        }

        private Person(String name, Integer age, Sex sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
        }

        public static Person getInstance() {
            return instatnce;
        }

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            out.writeInt(age);
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            age = in.readInt();
        }

        public String toString() {
            return "name: " + this.name + ", age: " + this.age + ", sex: " + this.sex;
        }
    }

    /**
     * 序列化
     */
    private static void serialize(String filename) throws IOException {
        File f = new File(filename); // 定義儲存路徑
        OutputStream out = new FileOutputStream(f); // 檔案輸出流
        ObjectOutputStream oos = new ObjectOutputStream(out); // 對象輸出流
        oos.writeObject(new Person("Jack", 30, Sex.MALE)); // 儲存對象
        oos.close();
        out.close();
    }

    /**
     * 反序列化
     */
    private static void deserialize(String filename) throws IOException, ClassNotFoundException {
        File f = new File(filename); // 定義儲存路徑
        InputStream in = new FileInputStream(f); // 檔案輸入流
        ObjectInputStream ois = new ObjectInputStream(in); // 對象輸入流
        Object obj = ois.readObject(); // 讀取對象
        ois.close();
        in.close();
        System.out.println(obj);
        System.out.println(obj == Person.getInstance());
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        final String filename = "d:/text.dat";
        serialize(filename);
        deserialize(filename);
    }
}           
name: Jack, age: 30, sex: MALE
false           

值得注意的是,從檔案中擷取的 Person 對象與 Person 類中的單例對象并不相等。為了能在單例類中仍然保持序列的特性,可以使用

readResolve()

方法。在該方法中直接傳回 Person 的單例對象。我們在 SerializeDemo04 示例的基礎上添加一個 readObject 方法, 如下所示:

public class SerializeDemo05 {
    // 其他内容略

    static class Person implements Serializable {

        // 添加此方法
        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            age = in.readInt();
        }
        // 其他内容略
    }

    // 其他内容略
}           
name: Jack, age: 30, sex: MALE
true           

總結

通過上面的内容,相各位已經了解了 Java 序列化的使用。這裡用一張腦圖來總結知識點。

Java 序列化Java 序列化

推薦閱讀

本文示例代碼見:

源碼

本文同步維護在:

Java 系列教程

參考資料

  • Java 程式設計思想(Thinking in java)
  • http://www.hollischuang.com/archives/1140
  • http://www.codenuclear.com/serialization-deserialization-java/
  • http://www.blogjava.net/jiangshachina/archive/2012/02/13/369898.html
  • https://github.com/giantray/stackoverflow-java-top-qa/blob/master/contents/what-is-a-serialversionuid-and-why-should-i-use-it.md