java序列化機制詳解

java序列化機制詳解

java序列化是將java對象保存在文件或者通過網絡傳輸的機制,通過實現接口Serializable或者Externalizable標識該類的對象可以序列化和反序列化。

如果希望保存java對象的狀態並在以後的某個時刻在內存中重建該對象,我們可以通過java序列化的機制實現。

Serializable關鍵字

標識類對象是可序列化的只要實現Serializable接口即可,該接口不包括任何字段和方法,它只是一個空的接口。如以下代碼片斷,Student類實現了該接口並自動獲得了序列化的能力。

//實現了Serializable接口的類自動擁有序列化的能力
public class Student implements Serializable {
}

對象的狀態是由成員變量決定的,所以序列化保存的是成員變量的值。static修飾的是類變量,序列化的時候static變量不會保存,另外,我們還可以通過java提供的transient關鍵字來顯示標識變量不需要序列化。因此java序列化的是除了static和transient修飾的其它成員變量。

ObjectOutputSteram和ObjectInputStream

如何使用java的序列化機制呢?

java提供了ObjectOutputStream和ObjectInputStream兩個對象輸入輸出流來實現序列化。假設Student類需要序列化,類的定義如下:

import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午7:30
*/
public class Student implements Serializable {

    private Integer age;
    private String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

下面代碼是將Student對象序列化的例子

該例中將對象序列化到了文件student.out中,通過ObjectOutputStream將對象jack寫到文件輸出流中。接着通過ObjectInputStream將jack對象反序列化並強制轉換爲Student類。(注意此時需要捕獲ClassNotFoundException類,假如此時JVM找不到Student Class對象,將會拋出該異常。)

import java.io.*;

/**
 * <p>文件描述: 對象序列化Demo</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午7:34
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            /**
             * 對象輸出流,具體輸出流是文件輸出
             */
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            Student jack = new Student(23, "jack");
            /**
             * 通過ObjectOutputStream將對象寫入到文件輸出流
             */
            oos.writeObject(jack);
            oos.close();
            /**
             * 對象輸入流,具體輸入是文件輸入流
             */
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            /**
             * 通過ObjectInputStream將對象從文件輸入流讀入
             */
            Student stu = (Student) ois.readObject();
            ois.close();
            System.out.println(stu);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

輸出:

Student{age=23, name='jack'}

若Student類未實現Serializable接口,將會拋出運行時異常 java.io.NotSerializableException。

transient關鍵字

若將Student的age字段設置爲transient的,反序化後age字段將是空值:

import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午7:40
*/
public class Student implements Serializable{

    //transient修飾的字段默認不會被序列化     
    private transient Integer age;
    private String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

Demo運行後的輸出爲:

Student{age=null, name='jack'}

可以看到輸出後的age爲null,transient修飾的字段將不會被序列化。

static修飾的成員變量

前面我們說過java序列化包括的是除了static和transient修飾的其它成員變量。假設將Student的name字段設置爲static,看下Demo輸出結果:

import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午7:55
*/
public class Student implements Serializable{

    private Integer age;
    private static String name = "student";

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

Demo輸出結果爲:

Student{age=23, name='jack'}

看到反序列化後name的值是”jack”而不是”student”,這是爲何?

這正是因爲不會序列化static修飾的變量而導致的結果。序列化後的文件其實name字段值爲空,反序列化後去方法區查找靜態變量name的值,這個name值正是之前Demo類通過Student的構造函數將name值設置爲”jack”的值,所以最終反序列化後的name值爲”jack”。

也就是說這個”name”值不是通過反序列化得到的,而是通過去方法區拿到的全局的值。

如果讀者對上例有疑惑,可以繼續看下面的例子:

import java.io.*;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午20:30
*/
public class Student implements Serializable{

    private Integer age;
    private static String name = "student";

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
    //增加了設置靜態變量的方法
    public static void setName(String name) {
        Student.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
import java.io.*;

/**
 * <p>文件描述: </p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午20:52
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            Student jack = new Student(23, "jack");
            oos.writeObject(jack);
            oos.close();
           //反序列化前將靜態變量設置爲"tom"
            Student.setName("tom");

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            Student stu = (Student) ois.readObject();
            ois.close();
            System.out.println(stu);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Demo輸出結果爲:

Student{age=23, name='tom'}

輸出結果的name值爲”tom”,而不是寫入時的”jack”。因爲在反序列化之前已經將靜態變量name的值設置爲”tom”,反序列化後會去取這個靜態變量的值。:smile:

writeObject()和readObject()方法

如果沒有爲類添加這兩個方法,序列化和反序列化是通過ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法進行的。

如果爲類添加writeObject()和readObject()方法,將不會再調用ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法,我們可以更加精確的操作對象的序列化。

例如如果將Student類的age字段設置爲transient了,此時我們又想去序列化該字段,那麼我們通過這兩個方法來實現:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午21:12
*/
public class Student implements Serializable{

    private transient Integer age;
    private String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        //默認的序列化
        oos.defaultWriteObject();
        oos.writeInt(age);
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        //默認的反序列化
        ois.defaultReadObject();
        this.age = ois.readInt();
    }

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

此時Demo的輸出結果將會輸出age的值:

Student{age=23, name='jack'}

注意,這兩個方法都是private的。方法中首先調用了序列化的默認方法,之後纔是自定義的邏輯。還要注意的是序列化的順序和反序列化的順序必須一致。否則會讀出未知的結果。例如將Student的age和name都設置爲transient,在writeObject和readObject方法中調整寫入和讀出的順序:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午21:29
*/
public class Student implements Serializable{

    private transient Integer age;
    private transient String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        //默認的序列化
        oos.defaultWriteObject();
        oos.writeInt(age);
        oos.writeUTF(name);
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        //默認的序列化
        ois.defaultReadObject();
        //此處讀入的順序和寫入的順序不一致
        this.name = ois.readUTF();
        this.age = ois.readInt();
    }

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

Demo輸出結果讓人莫名其妙:

Student{age=1507332, name=''}

此處如果將oos.writeUTF方法改爲oos.writeObject、ois.readUTF方法改爲ois.readObject,會拋出異常 OptionalDataException

Externalizable關鍵字

實現了該接口的類基於之前Serializable的序列化機制就會失效,Externalizable接口已經實現了Serializable。看下面的例子:

import java.io.*;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午21:53
*/
public class Student implements Externalizable{

    private Integer age;
    private String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

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

    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

    }
}

將Student改爲上面的代碼,運行Demo類的輸出結果爲:

java.io.InvalidClassException: com.lms.serializable.Student; no valid constructor

異常拋出,提示沒有可用的構造器,其實就是沒有默認的構造函數。實現了Externalizable接口的類對象,反序列化的時候會先通過無參的構造函數new一個對象,然後將各個字段的值賦值給該對象,Student沒有默認的構造函數,所以上例拋出異常。

爲Student添加默認構造函數後的Demo輸出結果爲:

Student{age=null, name='null'}

可以看出,所有字段爲空,這是因爲實現的Externalizable接口的對象序列化需要開發者自己去保證,也就是需要實現writeExternal和readExternal方法,實現了writeExternal和readExternal方法後的Student類如下:

import java.io.*;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午22:14
*/
public class Student implements Externalizable{

    private Integer age;
    private String name;

    public Student() {

    }

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

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

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

Demo輸出結果爲:

Student{age=23, name='jack'}

可以看出正確輸出了結果。

單例的序列化

如果一個類是單例,反序列化後是同一個對象嗎?我們將Student改爲單例模式的(Student用單例模式確實有點牽強),看下例:

import java.io.*;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午22:40
*/
public class Student implements Serializable{

    private Integer age;
    private String name;

    private static class Instance {
        private static Student jack = new Student(23, "jack");
    }

    private Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public static Student getInstance() {
        return Instance.jack;
    }

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

}

Demo類修改爲:

import java.io.*;

/**
 * <p>文件描述:Demo類 </p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午23:00
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            //取單例
            Student jack = Student.getInstance();
            oos.writeObject(jack);
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            Student stu = (Student) ois.readObject();
            ois.close();
            System.out.println(stu);
           //看反序列化後是否是同一個對象
            System.out.println(jack == stu);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Demo輸出結果爲:

Student{age=23, name='jack'}
false

可以看出,反序列化後不是同一個對象。那要怎麼解決這個問題呢?可以在單例類Student中加個私有方法readResolve()方法。

import java.io.*;

/**
 * <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午23:20
*/
public class Student implements Serializable{

    private Integer age;
    private String name;

    private static class Instance {
        private static Student jack = new Student(23, "jack");
    }

    private Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public static Student getInstance() {
        return Instance.jack;
    }
    //增加readResolve方法,直接返回單例
    private Object readResolve() {
        return Instance.jack;
    }

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

}

Demo輸出結果爲:

Student{age=23, name='jack'}
true

可以看到,反序列化後的對象和序列化之前的對象是同一個對象。

無論是實現Serializable或者Externalizable接口的對象,反序列化的時候都會調用readResolve方法,因此這裏的readResolve方法直接返回了單例,而不是返回反序列化後的對象,保證了單例的邏輯。

序列化ID

實現Serializable接口的類需要生成對應的序列化id(serialVersionUID)。如:

    private static final long serialVersionUID = -5881678404557689785L;

如果沒有顯示爲類添加該字段(IDE可以設置成自動生成),java將會根據字節碼文件動態生成序列化ID。每次重新編譯生成的序列化ID都不一樣。所以前面的Student例子都不是完美的,沒有生成序列化ID。

如果顯示爲類添加該字段,那麼不管編譯多少次,只要沒有修改該字段的值,該字段就不會變。

爲何需要序列化ID?

如果一個類X在多個客戶端使用,其中一個客戶端A收到來自客戶端B的類X對象x。客戶端A通過網絡收到對象x的二進制序列化文件後會進行反序列化操作。首先客戶端A會取出對象x的序列化ID,並將該ID和客戶端A本地類X Class對象的序列化ID進行比較,如果不相等那就會提示序列化失敗,如果相等那就可以正常進行序列化。本人所在的公司現在使用的RPC框架、阿里開源的dubbo所使用的網絡傳輸對象就是實現了Serializable接口。

父類和子類實現序列化的問題

  • 父類實現了序列化接口,子類自動擁有序列化功能,不需要顯示的實現序列化接口
  • 父類沒有實現序列化接口,子類實現了序列化接口。序列化子類對象時不會去序列化父類的對象。因爲必須有父對象纔有子對象,反序列化時會調用父類的無參默認構造函數,因此父類必須要有無參默認構造函數,反序列化後父類對象字段值都是其類型默認值(基本類型是0值,引用類型是null)

序列化多次相同對象的問題

如果序列化兩個相同的對象,java做了特殊處理,看下例:

import java.io.*;

/**
 * <p>文件描述: Demo類</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午23:40
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            Student jack = new Student(23, "jack");
            //第一次序列化對象
            oos.writeObject(jack);
            oos.flush();
            //第一次序列化對象後的文件大小
            System.out.println(new File("student.out").length());
            //第二次序列化同一個對象
            oos.writeObject(jack);
            //第二次序列化對象後的文件大小
            System.out.println(new File("student.out").length());
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            //第一次反序列化對象
            Student stu1 = (Student) ois.readObject();
            //第二次反序列化對象
            Student stu2 = (Student) ois.readObject();
            ois.close();
            //比較是否同一個對象
            System.out.println(stu1 == stu2);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Demo輸出結果爲:

189
194
true

可以看到兩次反序列化後的對象是同一個對象。

第一次序列化後的文件大小爲189,第二次序列化後的文件大小爲194,只增加了5字節,這顯示是java做了優化導致的。兩個相同對象當然不需要冗餘存儲,這增加的5字節是一些引用信息,這些引用指向同一個對象,反序列化後重建引用關係。

另外需要注意的是,如果在第二次序列化同一個對象之前,改變了這個對象某些字段的值,因爲虛擬機發現之前已經將該對象序列化過了,這時只會存儲寫的引用,不會實際去寫,所以第二次序列化之前對這些字段的改變實際上是沒有效果的。

例如:

import java.io.*;

/**
 * <p>文件描述:Demo類 </p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午23:54
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            Student jack = new Student(23, "jack");
            //第一次序列化對象
            oos.writeObject(jack);
            oos.flush();
            //第一次序列化對象後的文件大小
            System.out.println(new File("student.out").length());
            //第二次序列化之前將name值改爲"lucy"
            jack.setName("lucy");
            //第二次序列化同一個對象
            oos.writeObject(jack);
            //第二次序列化對象後的文件大小
            System.out.println(new File("student.out").length());
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            //第一次反序列化對象
            Student stu1 = (Student) ois.readObject();
            //第二次反序列化對象
            Student stu2 = (Student) ois.readObject();
            ois.close();
            //比較是否同一個對象
            System.out.println(stu1);
            System.out.println(stu2);
            System.out.println(stu1 == stu2);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

輸出結果爲:

189
194
Student{age=23, name='jack'}
Student{age=23, name='jack'}
true

可以發現輸出的兩個對象值完全相同,第二次反序列化之前將值改爲”lucy”是無效的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章