1. 程式人生 > >Java高階系列——不得不說的物件序列化(serialize)

Java高階系列——不得不說的物件序列化(serialize)

1、什麼是Java物件序列化?

Java的物件序列化是將那些實現了Serializable介面的物件轉化成一個位元組序列,並能夠在以後將這些位元組序列完全恢復成原來的物件。簡單來說序列化就是將物件轉化成位元組流,反序列化就是將位元組流轉化成物件。

物件必須在程式中顯示的序列化(serialize)和反序列化(deserialize)。

2、序列化的作用

序列化的主要用途主要有兩個,一個是物件持久化,另一個是跨網路的資料交換、遠端過程呼叫。

物件持久化意味著一個物件的生存週期並不取決於程式是否正在執行,他可以生存與程式的呼叫之間。通過將一個序列化的物件寫入磁碟,然後在重新呼叫程式時恢復該物件,就能夠實現持久化的效果。

序列化能夠彌補不同作業系統之間的差異,比如說可以在執行Windows系統的計算機上建立一個物件,然後將其序列化,通過網路將它傳送給一臺執行Linux系統的計算機,然後在那裡準確的重新組裝而不必擔心資料在不同的機器上的表示會不同,也不必關心位元組的順序或者其他任何細節,使得物件在其他機器上就像在本地機器上一樣。當向遠端物件傳送訊息時,需要通過物件序列化來傳輸引數和返回值。

3、基本實現

要讓一個類支援序列化,只需要讓這個類實現介面java.io.Serializable,Serializable沒有定義任何方法,只是一個標記介面。

物件序列化是基於位元組的,因此要使用OutputStream和InputStream繼承層次結構。

序列化物件:建立某些OutputStream物件,然後將其封裝在一個ObjectOutputStream物件內,之後呼叫writeObject()方法序列化物件。

反序列化物件:建立某些InputStream物件,然後將其封裝在一個ObjectInputStream物件內,之後呼叫readObject()方法反序列化物件。反序列化最後獲得的是一個指向Object的引用,所以最後必須向下轉型為指定型別的物件。

我們定義一個Student類,讓該類實現Serializable介面,然後我們通過以上所說的序列化和反序列化方法來親身感受一下序列化的魔力。

package io;
import
java.io.Serializable; public class Student implements Serializable { /** * @Comment * @Author Ron * @Date 2018年4月9日 上午11:41:41 */ private static final long serialVersionUID = 1L; public Student(String no,String name,String className) { this.no = no; this.name = name; this.className = className; } public String toString() { return "HashCode:"+hashCode()+" 學號:"+no+" 姓名:"+name+" 班級:"+className; } /** * 學號 */ private String no; /** * 名字 */ private String name; /** * 班級 */ private String className; public String getNo() { return no; } public void setNo(String no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getClassName() { return className; } public void setClassName(String className) { this.className = className; } }

我們定義一個序列化的測試類SerializableTest,具體實現如下:

package io;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class SerializableTest {

    /**
     * @Comment 序列化
     * @Author Ron
     * @Date 2018年4月9日 上午11:45:38
     * @return
     */
    public static void writeStudents(List<Student> students) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("students.dat"));

        out.writeObject(students);

        out.close();
    }

    /**
     * @Comment 反序列化物件
     * @Author Ron
     * @Date 2018年4月9日 上午11:49:07
     * @return
     */
    public static List<Student> readStudents() throws IOException,ClassNotFoundException  {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("students.dat"));

        List<Student> list = (List<Student>) in.readObject();

        in.close();

        return list;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<Student> students = new ArrayList<>(Arrays.asList(new Student("001", "Ron", "Class 001"),new Student("002", "Ron2", "Class 002")));
        System.out.println("------------------序列化前--------------");
        System.out.println(students);
        System.out.println("------------------反序列化後--------------");
        writeStudents(students);
        List<Student> students1 = readStudents();
        System.out.println(students1);
    }
}

執行上面的程式我們會得到一個類似於如下的結果:

------------------序列化前--------------
[HashCode:366712642 學號:001 姓名:Ron 班級:Class 001, HashCode:1829164700 學號:002 姓名:Ron2 班級:Class 002]
------------------反序列化後--------------
[HashCode:1836019240 學號:001 姓名:Ron 班級:Class 001, HashCode:325040804 學號:002 姓名:Ron2 班級:Class 002]

我們通過結果可以看到,序列化前和序列化後再通過反序列化重新獲得的物件資料基本是一致的(這裡需要注意,序列化前的物件和反序列化重新獲取的物件,在他們的欄位資料上是一致的,但是需要注意的是,反序列化是重新生成了物件,並不修改原來的物件)。

序列化時我們需要知道的是:ObjectOutputStream是OutputStream的子類,但實現了ObjectOutput介面,ObjectOutput是DataOutput的子介面,增加了一個方法:

public void writeObject(Object obj) throws IOException

這個方法能夠將物件obj轉化為位元組,寫到流中。

反序列化時我們需要知道的是:ObjectInputStream是InputStream的子類,它實現了ObjectInput介面,ObjectInput是DataInput的子介面,增加了一個方法:

public Object readObject() throws ClassNotFoundException, IOException

這個方法能夠從流中讀取位元組,轉化為一個物件。

4、複雜物件

我們上面說描述的是一個非常簡單的例項,我們現在來考慮一個稍微複雜一點的情況。如果現在有兩個Student物件,這兩個Student物件都引用了同一個DeskTop物件(兩個同學一張課桌),那麼反序列化之後還能讓反序列化之後的兩個Student物件引用了同一個DeskTop物件嗎?我們來看一下。

public class DeskTop implements Serializable {
    /**
     * @Comment 
     * @Author Ron
     * @Date 2018年4月9日 下午1:58:42
     */
    private static final long serialVersionUID = 1L;
    private Double height;
    private Double width;
    private Double length;

    public String toString() {
        return "height:"+height+" width:"+width+" length:"+length;
    }

    public Double getHeight() {
        return height;
    }
    public void setHeight(Double height) {
        this.height = height;
    }
    public Double getWidth() {
        return width;
    }
    public void setWidth(Double width) {
        this.width = width;
    }
    public Double getLength() {
        return length;
    }
    public void setLength(Double length) {
        this.length = length;
    }
}

要讓Student能夠引用DeskTop物件,我們需要改造一下上面我們的Student類(增加DeskTop屬性,修改建構函式)如下;

public Student(String no,String name,String className,DeskTop deskTop) {
    this.no = no;
    this.name = name;
    this.className = className;
    this.setDeskTop(deskTop);
}
private DeskTop deskTop;

我們建立兩個Student物件和一個DeskTop物件,讓兩個Student引用同一個DeskTop物件。

public class SerializableTest2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        DeskTop deskTop = new DeskTop();
        deskTop.setHeight(100d);
        deskTop.setLength(200d);
        deskTop.setWidth(50d);

        Student stdu1 = new Student("001", "Ron", "Class 001",deskTop);
        Student stdu2 = new Student("002", "Ron02", "Class 002",deskTop);

        System.out.println("------------------序列化前--------------");
        if(stdu1.getDeskTop().equals(stdu2.getDeskTop())){
            System.out.println("reference the same object:"+stdu2.getDeskTop());
        }

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("students.dat"));

        out.writeObject(stdu1);
        out.writeObject(stdu2);

        out.close();
        System.out.println("------------------反序列化後--------------");

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("students.dat"));
        Student stud3 = (Student) in.readObject();
        Student stud4 = (Student) in.readObject();
        in.close();

        if(stud3.getDeskTop().equals(stud4.getDeskTop())){
            System.out.println("reference the same object:"+stud4.getDeskTop());
        }
    }
}

執行程式,我們會得到如下的結果:

------------------序列化前--------------
reference the same object:height:100.0 width:50.0 length:200.0
------------------反序列化後--------------
reference the same object:height:100.0 width:50.0 length:200.0

這也是Java序列化機制的神奇之處,它能自動處理這種引用同一個物件的情況。更神奇的是,它
還能自動處理迴圈引用的情況,這裡我們就不給出具體的例子,讀者可自行事件。

5、序列化控制

預設的序列化機制已經很強大了,它可以自動將物件中的所有欄位自動儲存和恢復,但這種預設行為有時候不是我們想要的。比如,對於有些欄位,它的值可能與記憶體位置有關,比如預設的hashCode()方法的返回值,當恢復物件後,記憶體位置肯定變了,基於原記憶體位置的值也就沒有了意義。還有一些欄位,可能與當前時間有關,比如表示物件建立時的時間,儲存和恢復這個欄位就是不正確的。

還有一些情況,如果類中的欄位表示的是類的實現細節,而非邏輯資訊,那預設序列化也是不適合的。為什麼不適合呢?因為序列化格式表示一種契約,應該描述類的邏輯結構,而非與實現細節相繫結,繫結實現細節將使得難以修改,破壞封裝。

Java提供了多種定製序列化的機制,主要的有三種,一種是transient關鍵字,另外一種是實現Externalizable介面代替實現Serializable,還有一種是實現writeObject和readObject方法。

將欄位宣告為transient,預設序列化機制將忽略該欄位,不會進行儲存和恢復。
比如上面的第一個例項中,假設我們在進行序列化和反序列化時不需要儲存和恢復no欄位的資訊,那麼我們可以在no欄位前面加上一個transient修飾符。

private transient String no;

執行程式我們會得到如下的結果:

------------------序列化前--------------
[HashCode:366712642 學號:001 姓名:Ron 班級:Class 001, HashCode:1829164700 學號:002 姓名:Ron2 班級:Class 002]
------------------反序列化後--------------
[HashCode:2133927002 學號:null 姓名:Ron 班級:Class 001, HashCode:1836019240 學號:null 姓名:Ron2 班級:Class 002]

我們可以到no欄位的內容反序列化之後變成了null。將欄位宣告為transient,不是說就不儲存該欄位了,而是告訴Java預設序列化機制,不要自動儲存該欄位了。

6、實現Externalizable介面對序列化進行控制

我們定義一個類Student2,其欄位資訊與我們上述的Student一致,但是不同的是,我們的Student2類實現的是Externalizable介面而不是Serializable。實現Externalizable介面必須實現如下兩個方法:

public void writeExternal(ObjectOutput out) throws IOException
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException

同時實現Externalizable介面必須保證實現的類具有無參構造器,預設情況下如果在你的類中沒有顯示宣告構造器,那麼你可以不用關心這個問題。但是如果你已經顯示的宣告過構造器(比如我們的Student2中聲明瞭有參的構造器,那麼我們就必須宣告一個無參構造器),那麼你就必須宣告一個無參的構造器,否則在進行序列化時會報no valid constructor異常。

Student2中關鍵程式碼如下:

public class Student2 implements Externalizable {

    /**
     * @Comment 
     * @Author Ron
     * @Date 2018年4月9日 上午11:41:41
     */
    private static final long serialVersionUID = 1L;

    public Student2() {
        System.out.println("Student2 Constructor");
    }

    public Student2(String no,String name,String className,DeskTop deskTop) {
        this.no = no;
        this.name = name;
        this.className = className;
        this.setDeskTop(deskTop);
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("------------------writeExternal--------------");
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("------------------readExternal--------------");
    }
}

我們開發一個測試程式如下:

public class SerializableTest3 {

    /**
     * @Comment 序列化
     * @Author Ron
     * @Date 2018年4月9日 上午11:45:38
     * @return
     */
    public static void writeStudents(List<Student2> students) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("students.dat"));
        System.out.println("------------------開始序列化--------------");
        out.writeObject(students);
        System.out.println("------------------序列化結束--------------");
        out.close();
    }

    /**
     * @Comment 反序列化物件
     * @Author Ron
     * @Date 2018年4月9日 上午11:49:07
     * @return
     */
    public static List<Student2> readStudents() throws IOException,ClassNotFoundException  {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("students.dat"));
        System.out.println("------------------開始反序列化--------------");
        List<Student2> list = (List<Student2>) in.readObject();
        System.out.println("------------------反序列化結束--------------");
        in.close();

        return list;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<Student2> students = new ArrayList<>(Arrays.asList(new Student2("001", "Ron", "Class 001",null),new Student2("002", "Ron2", "Class 002",null)));
        System.out.println("------------------序列化前--------------");
        System.out.println(students);
        writeStudents(students);
        List<Student2> students1 = readStudents();
        System.out.println(students1);
    }
}

我們執行程式,會得到如下的結果:

------------------序列化前--------------
[HashCode:366712642 學號:001 姓名:Ron 班級:Class 001, HashCode:1829164700 學號:002 姓名:Ron2 班級:Class 002]
------------------開始序列化--------------
------------------writeExternal--------------
------------------writeExternal--------------
------------------序列化結束--------------
------------------開始反序列化--------------
Student2 Constructor
------------------readExternal--------------
Student2 Constructor
------------------readExternal--------------
------------------反序列化結束--------------
[HashCode:1252169911 學號:null 姓名:null 班級:null, HashCode:2101973421 學號:null 姓名:null 班級:null]

通過結果我們可以看到:

  1. 在序列化和反序列化過程中writeExternal(ObjectOutput out)和readExternal(ObjectInput in)會自動的被呼叫。上面的示例中我們在這兩個方法中並未做過多操作,所以通過反序列化獲得的結果都為null。

  2. 同時我們還可以發現,在反序列化時,無參的構造器都會先被呼叫。這與恢復一個Serializable物件不同,對於Serializable物件,物件完全以他儲存的二進位制為基礎來構造,而不呼叫構造器。而對於一個Externalizable物件,所有普通的無參(預設)構造器都會被呼叫,然後呼叫readExternal(ObjectInput in)。必須注意這一點,所有普通的無參(預設)構造器都會被呼叫,才能使Externalizable物件產生正確的行為。

那如果在反序列化時我們想要完全恢復物件時怎麼處理?我們只需要按照正確的方式實現writeExternal(ObjectOutput out)和readExternal(ObjectInput in)即可。我們將Student2 中的這兩個方法按照如下修改:

@Override
public void writeExternal(ObjectOutput out) throws IOException {
    System.out.println("------------------writeExternal--------------");
    out.writeObject(no);
    out.writeObject(name);
    out.writeObject(className);
    out.writeObject(deskTop);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    System.out.println("------------------readExternal--------------");
    no = (String) in.readObject();
    name = (String) in.readObject();
    className = (String) in.readObject();
    deskTop = (DeskTop) in.readObject();
}

然後我們執行一下上面的程式,我們會得到如下結果:

------------------序列化前--------------
[HashCode:366712642 學號:001 姓名:Ron 班級:Class 001, HashCode:1829164700 學號:002 姓名:Ron2 班級:Class 002]
------------------開始序列化--------------
------------------writeExternal--------------
------------------writeExternal--------------
------------------序列化結束--------------
------------------開始反序列化--------------
Student2 Constructor
------------------readExternal--------------
Student2 Constructor
------------------readExternal--------------
------------------反序列化結束--------------
[HashCode:21685669 學號:001 姓名:Ron 班級:Class 001, HashCode:2133927002 學號:002 姓名:Ron2 班級:Class 002]

7、實現Serializable介面的類中新增writeObject和readObject方法

如果不是特別堅持實現Externalizable 介面,那麼我們可以在實現了Serializable介面的類中新增如下兩個方法來對序列化進行控制。

private void writeObject(ObjectOutputStream stream) throws IOException
private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException

這樣一旦物件被序列化或者被反序列化還原,就會自動的分別呼叫這兩個方法,也就是說只要我們提供這兩個方法,就會使用他們而不是預設的序列化機制。

我們先來看例子,我們建立一個類,命名為Student3並實現Serializable介面,類中的屬性資訊與上文所述的Student一致,不一樣的就是在Student3中我們添加了以下兩個方法:

private void writeObject(ObjectOutputStream stream) throws IOException {
    System.out.println("------------------Student.writeObject--------------");
    stream.writeObject(no);
}

private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException {
    System.out.println("------------------Student.readObject--------------");
    no = (String) stream.readObject();
}

我們再新建一個測試類,具體程式碼如下:

public class SerializableTest4 {

    /**
     * @Comment 序列化
     * @Author Ron
     * @Date 2018年4月9日 上午11:45:38
     * @return
     */
    public static void writeStudent3s(List<Student3> Student3s) throws IOException {
        System.out.println("------------------開始序列化--------------");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Student3s.dat"));

        out.writeObject(Student3s);

        out.close();
        System.out.println("------------------序列化結束--------------");
    }

    /**
     * @Comment 反序列化物件
     * @Author Ron
     * @Date 2018年4月9日 上午11:49:07
     * @return
     */
    public static List<Student3> readStudent3s() throws IOException,ClassNotFoundException  {
        System.out.println("------------------開始反序列化--------------");

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("Student3s.dat"));

        List<Student3> list = (List<Student3>) in.readObject();

        in.close();
        System.out.println("------------------反序列化結束--------------");
        return list;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<Student3> Student3s = new ArrayList<>(Arrays.asList(new Student3("001", "Ron", "Class 001",null),new Student3("002", "Ron2", "Class 002",null)));
        System.out.println("------------------序列化前--------------");
        System.ou