Java ---- 序列化

Java對象的序列化

  • Java平臺容許咱們在內存中建立可複用的Java對象,但通常狀況下,只有當JVM處於運行時,這些對象纔可能存在,即,這些對象的生命週期不會比JVM的生命週期更長。但在現實應用中,就可能要求在JVM中止運行以後可以保存(持久化)指定的對象,並在未來從新讀取被保存的對象。Java對象序列化就可以幫助咱們實現該功能。java

  • 使用Java對象序列化,在保存對象時,會把其狀態保存爲一組字節,在將來,再將這些字節組裝成對象。必須注意地是,對象序列化保存的是對象的」狀態」,即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量程序員

  • 簡而言之,就是讓對象想基本數據類型和字符串類型同樣,經過輸入輸出字節流ObjectInputStream 和 ObjectOutputStream進行寫和讀操做。數據庫

Java序列化的應用場景

  • 當你想把的內存中的對象狀態保存到一個文件中或者數據庫中時候數組

  • 當你想用套接字在網絡上傳送對象的時候網絡

  • 當你想經過RMI傳輸對象的時候ide

如何對Java對象進行序列化與反序列化

在Java中,只要一個類實現了java.io.Serializable接口,那麼它就能夠被序列化。函數

import java.io.*;
import java.util.*;

class User implements Serializable {
    private String name;
    private int age;
    private Date birthday;
    private transient String gender;
    private static int test =1;
    private static final long serialVersionUID = -6849794470754667710L;

    public User() {
        System.out.println("none-arg constructor");
    }

    public User(String name, Integer age,Date birthday,String gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.birthday = birthday;
        this.gender = gender;
    }

    public void setTest(int Newtest) {
        this.test = Newtest;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", birthday=" + birthday +
                ", testStatic="+test+
                '}'+" "+super.toString();
    }
}

public class SerializableDemo {
    public static void main(String[] args) throws Exception {
        //Initializes The Object
        User user = new User("qiuyu",23,new Date(),"male");
        System.out.println(user);
        user.setTest(10);
        System.out.println(user);

        //Write Obj to File
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempFile"));
        out.writeObject(user);
        out.close();

        //Read Obj from File
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("tempFile"));
        User newUser = (User) in.readObject();
        System.out.println(newUser);
        in.close();
    }
}

此時的輸出爲: 測試

arg constructor
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 20:38:57 GMT+08:00 2017, testStatic=10} User@326de728
User{name='qiuyu', age=23, gender=null, birthday=Tue Nov 14 20:38:57 GMT+08:00 2017, testStatic=10} User@4883b407
  • 此時必須注意的是,當從新讀取被保存的User對象時,並無調用User的任何構造器,看起來就像是直接使用字節將User對象還原出來的。this

  • 當User對象被保存到tempfile文件中以後,咱們能夠在其它地方去讀取該文件以還原對象,但必須確保該讀取程序的CLASSPATH中包含有User.class,不然會拋出ClassNotFoundException。code

Q:以前不是說序列化不保存靜態變量麼,爲何這裏的靜態變量進行了傳遞,都變成了10呢?
A:由於此時User.class已經被加載進了內存中,且將static變量test從1更改成了10。當咱們恢復對象時,會直接獲取當前static的變量test的值,因此爲10。

Q:但若是咱們的恢復操做在另外一個文件中進行,結果會怎麼樣呢?

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class Recovering {
    public static void main(String[] args) throws Exception{
        //Read Obj from File
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("tempFile"));
        User newUser = (User) in.readObject();
        System.out.println(newUser);
        in.close();
    }
}

輸出結果爲:

User{name='qiuyu', age=23, gender=null, birthday=Tue Nov 14 20:38:57 GMT+08:00 2017, testStatic=1} User@6442b0a6

A:由於在運行此代碼時,User.class會被加載進內存,而後執行初始化,將被初始化爲1,所以test變量會被恢復爲1。注意區分上面兩種狀況,不要由於是在本地進行測試,就認爲static會被序列化,同時要了解Java運行時,內存加載的機制。

基本知識點

Serializable接口

  • 對於任何須要被序列化的對象,都必需要實現接口Serializable,它只是一個標識接口自己沒有任何成員,只是用來標識說明當前的實現類的對象能夠被序列化.

  • 若是父類實現序列化,子類自動實現序列化,不須要顯式實現Serializable接口。

  • 若是被寫對象的類型是String數組EnumSerializable,那麼就能夠對該對象進行序列化,不然將拋出NotSerializableException。

對象的讀寫

  • Java類中對象的序列化工做是經過 ObjectOutputStream ObjectInputStream 來完成的。

  • 使用readObject()writeObject()方法對對象進行讀寫操做;

  • 對於基本類型,可使用readInt()writeInt() , readDouble()writeDouble()等相似的接口進行讀寫。

序列化機制

  • 若是僅僅只是讓某個類實現Serializable接口,而沒有其它任何處理的話,則就是使用默認序列化機制。使用默認機制,在序列化對象時,不只會序列化當前對象自己,還會對該對象引用的其它對象也進行序列化,一樣地,這些其它對象引用的另外對象也將被序列化。

  • 在現實應用中,有些時候不能使用默認序列化機制。好比,但願在序列化過程當中忽略掉敏感數據,或者簡化序列化過程。爲此須要爲某個字段聲明爲transient,那麼序列化機制就會忽略被transient修飾的字段。transient的引用變量會以null返回,基本數據類型會以相應的默認值返回。(例如:引用類型沒有實現Serializable,或者動態數據只能夠在執行時求出而不能或沒必要存儲)

writeObject()與readObject()

  • 對於上被聲明爲transient的字段gender,除了將transient關鍵字去掉以外,是否還有其它方法能使它再次可被序列化?方法之一就是在User類中添加兩個方法:writeObject()與readObject(),以下所示:

private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeUTF(gender);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        gender = in.readUTF();
    }
  • 在writeObject()方法中會先調用ObjectOutputStream中的defaultWriteObject()方法,該方法會執行默認的序列化機制,此時會忽略掉gender字段。

  • 而後再調用writeUtf()方法顯示地將gender字段寫入到ObjectOutputStream中。readObject()的做用則是針對對象的讀取,其原理與writeObject()方法相同。

Q: writeObject()與readObject()都是private方法,那麼它們是如何被調用的呢?
A: 毫無疑問,是使用反射。(注意這不是繼承接口的方法,由於接口類的方法都是public的,而這裏的方法是private的)

Externalizable接口

  • 不管是使用transient關鍵字,仍是使用writeObject()和readObject()方法,其實都是基於Serializable接口的序列化。JDK中提供了另外一個序列化接口Externalizable,使用該接口以後,以前基於Serializable接口的序列化機制就將失效。

import java.io.*;
import java.util.*;

class UserExtern implements Externalizable {
    private String name;
    private int age;
    private Date birthday;
    private transient String gender;
    private static int test =1;
    private static final long serialVersionUID = -6849794470754667710L;

    public UserExtern() {
        System.out.println("none-arg constructor");
    }

    public UserExtern(String name, Integer age,Date birthday,String gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.birthday = birthday;
        this.gender = gender;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeUTF(gender);
    }

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


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

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
        birthday = (Date) in.readObject();
        gender = in.readUTF();
    }


    public void setTest(int Newtest) {
        this.test = Newtest;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", birthday=" + birthday +
                ", testStatic="+test+
                '}'+" "+super.toString();
    }
}



public class ExternalizableDemo {
    public static void main(String[] args) throws Exception {
        UserExtern userExtern = new UserExtern("qiuyu",23,new Date(),"male");
        System.out.println(userExtern);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("external"));
        out.writeObject(userExtern);
        out.close();


        ObjectInputStream in = new ObjectInputStream(new FileInputStream("external"));
        UserExtern userExtern1 = (UserExtern)in.readObject();
        System.out.println(userExtern1)
    }
}

輸出結果爲:

arg constructor
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:34:25 GMT+08:00 2017, testStatic=1} UserExtern@25618e91
none-arg constructor
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:34:25 GMT+08:00 2017, testStatic=1} UserExtern@604ed9f0
  • Externalizable繼承於Serializable,當使用該接口時,序列化的細節須要由程序員去完成writeExternal()readExternal()方法的具體細節,以及哪些狀態須要進行序列化。

  • 另外,若使用Externalizable進行序列化,當讀取對象時,會調用被序列化類的無參構造器去建立一個新的對象,而後再將被保存對象的字段的值分別填充到新對象中。這就是爲何在此序列化過程當中UserExtern的無參構造器會被調用。因爲這個緣由,實現Externalizable接口的類必需要提供一個無參的構造器,且它的訪問權限爲public

注意事項

  • 讀取對象的順序必須與寫入的順序相同

  • 若是有不能被序列化的對象,執行期間就會拋出NotSerializableException異常

  • 序列化時,只對對象的狀態進行保存,而無論對象的方法

  • 靜態變量不會被序列化,由於全部的對象共享同一份靜態變量的值

  • 若是一個對象的成員變量是一個對象,那麼這個對象的數據成員也會被保存還原,並且會是遞歸的方式(對象網)。(序列化程序會將對象版圖上的全部東西儲存下來,這樣才能讓該對象恢復到原來的狀態)

  • 若是子類實現Serializable接口而父類未實現時,父類不會被序列化,但此時父類必須有個無參構造方法,不然會拋InvalidClassException異常;由於反序列化時會恢復原有子對象的狀態,而父類的成員變量也是原有子對象的一部分。因爲父類沒有實現序列化接口,即便沒有顯示調用,也會默認執行父類的無參構造函數使變量初始化;

深刻理解

序列化ID的問題

  • serialVersionUID適用於JAVA的序列化機制。簡單來講,Java的序列化機制是經過判斷類的serialVersionUID來驗證版本一致性的。

  • 在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,若是相同就認爲是一致的,能夠進行反序列化,不然就會出現序列化版本不一致的異常,便是InvalidCastException。

序列化存儲規則

  • Java 序列化機制爲了節省磁盤空間,具備特定的存儲規則,當寫入文件的爲同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用;

  • 序列化到同一個文件時,如第二次修改了相同對象屬性值再次保存時候,虛擬機根據引用關係知道已經有一個相同對象已經寫入文件,所以只保存第二次寫的引用,因此讀取時,都是第一次保存的對象,第二次進行的修改將無效。

public static void main(String[] args) throws Exception {
        //Initializes The Object
        User user = new User("qiuyu",23,new Date(),"male");
        user.setTest(10);
        System.out.println(user);


        //Write Obj to File
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempFile"));
        out.writeObject(user);
        user.setAge(25);
        System.out.println(user);
        out.writeObject(user);
        out.close();

        //Read Obj from File
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("tempFile"));
        User newUser = (User) in.readObject();
        User newUser1 = (User) in.readObject();
        System.out.println(newUser);
        System.out.println(newUser1);
        in.close();
    }

輸出結果: 注意觀察age的值

User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@326de728
User{name='qiuyu', age=25, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@326de728
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@4883b407
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@4883b407

屢次序列化的問題

  • 在一次的序列化的過程當中,ObjectOutputStream 會在文件開始的地方寫入一個 Header的信息到文件中。因而在屢次序列化的過程當中就會繼續在文件末尾(本次序列化的開頭)寫入 Header 的信息,這時若是進行反序列化的對象的時候會報錯:java.io.StreamCorruptedException: invalid type code: AC

相關文章
相關標籤/搜索