java基礎——Java對象的序列化與反序列化

轉自http://www.importnew.com/17964.html

序列化與反序列化

序列化 (Serialization)是將對象的狀態信息轉換爲可以存儲或傳輸的形式的過程。一般將一個對象存儲至一個儲存媒介,例如檔案或是記億體緩衝等。在網絡傳輸過程中,可以是字節或是XML等格式。而字節的或XML編碼格式可以還原完全相等的對象。這個相反的過程又稱爲反序列化。

Java對象的序列化與反序列化

在Java中,我們可以通過多種方式來創建對象,並且只要對象沒有被回收我們都可以複用該對象。但是,我們創建出來的這些Java對象都是存在於JVM的堆內存中的。只有JVM處於運行狀態的時候,這些對象纔可能存在。一旦JVM停止運行,這些對象的狀態也就隨之而丟失了。

但是在真實的應用場景中,我們需要將這些對象持久化下來,並且能夠在需要的時候把對象重新讀取出來。Java的對象序列化可以幫助我們實現該功能。

對象序列化機制(object serialization)是Java語言內建的一種對象持久化方式,通過對象序列化,可以把對象的狀態保存爲字節數組,並且可以在有需要的時候將這個字節數組通過反序列化的方式再轉換成對象。對象序列化可以很容易的在JVM中的活動對象和字節數組(流)之間進行轉換。

在Java中,對象的序列化與反序列化被廣泛應用到RMI(遠程方法調用)及網絡傳輸中。


相關接口及類

Java爲了方便開發人員將Java對象進行序列化及反序列化提供了一套方便的API來支持。其中包括以下接口和類:

java.io.Serializable

java.io.Externalizable

ObjectOutput

ObjectInput

ObjectOutputStream

ObjectInputStream

Serializable 接口

類通過實現 java.io.Serializable 接口以啓用其序列化功能。未實現此接口的類將無法使其任何狀態序列化或反序列化。可序列化類的所有子類型本身都是可序列化的。序列化接口沒有方法或字段,僅用於標識可序列化的語義。 (該接口並沒有方法和字段,爲什麼只有實現了該接口的類的對象才能被序列化呢?)

當試圖對一個對象進行序列化的時候,如果遇到不支持 Serializable 接口的對象。在此情況下,將拋出NotSerializableException

如果要序列化的類有父類,要想同時將在父類中定義過的變量持久化下來,那麼父類也應該集成java.io.Serializable接口。

下面是一個實現了java.io.Serializable接口的類

public class User1 implements Serializable {
 
    private String name;
    private int age;
 
    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;
    }
 
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

通過下面的代碼進行序列化及反序列化

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
 
import java.io.*;
/**
 * Created by hollis on 16/2/17.
 * SerializableDemo1 結合SerializableDemo2說明 一個類要想被序列化必須實現Serializable接口
 */
public class SerializableDemo1 {
 
    public static void main(String[] args) {
        //Initializes The Object
        User1 user = new User1();
        user.setName("hollis");
        user.setAge(23);
        System.out.println(user);
 
        //Write Obj to File
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(oos);
        }
 
        //Read Obj from File
        File file = new File("tempFile");
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(file));
            User1 newUser = (User1) ois.readObject();
            System.out.println(newUser);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(ois);
            try {
                FileUtils.forceDelete(file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
 
    }
}
 
//OutPut:
//User{name='hollis', age=23}
//User{name='hollis', age=23}


Externalizable接口

除了Serializable 之外,java中還提供了另一個序列化接口Externalizable

爲了瞭解Externalizable接口和Serializable接口的區別,先來看代碼,我們把上面的代碼改成使用Externalizable的形式。

<pre name="code" class="java">import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
 
/**
 * Created by hollis on 16/2/17.
 * 實現Externalizable接口
 */
public class User1 implements Externalizable {
 
    private String name;
    private int age;
 
    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 void writeExternal(ObjectOutput out) throws IOException {
 
    }
 
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
 
    }
 
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}





import java.io.*;
 
/**
 * Created by hollis on 16/2/17.
 */
public class ExternalizableDemo1 {
 
    //爲了便於理解和節省篇幅,忽略關閉流操作及刪除文件操作。真正編碼時千萬不要忘記
    //IOException直接拋出
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //Write Obj to file
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        User1 user = new User1();
        user.setName("hollis");
        user.setAge(23);
        oos.writeObject(user);
        //Read Obj from file
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        User1 newInstance = (User1) ois.readObject();
        //output
        System.out.println(newInstance);
    }
}
//OutPut:
//User{name='null', age=0}

通過上面的實例可以發現,對User1類進行序列化及反序列化之後得到的對象的所有屬性的值都變成了默認值。也就是說,之前的那個對象的狀態並沒有被持久化下來。這就是Externalizable接口和Serializable接口的區別:

Externalizable繼承了Serializable,該接口中定義了兩個抽象方法:writeExternal()readExternal()。當使用Externalizable接口來進行序列化與反序列化的時候需要開發人員重寫writeExternal()readExternal()方法。由於上面的代碼中,並沒有在這兩個方法中定義序列化實現細節,所以輸出的內容爲空。還有一點值得注意:在使用Externalizable進行序列化的時候,在讀取對象時,會調用被序列化類的無參構造器去創建一個新的對象,然後再將被保存對象的字段的值分別填充到新對象中。所以,實現Externalizable接口的類必須要提供一個public的無參的構造器。

按照要求修改之後代碼如下:

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
 
/**
 * Created by hollis on 16/2/17.
 * 實現Externalizable接口,並實現writeExternal和readExternal方法
 */
public class User2 implements Externalizable {
 
    private String name;
    private int age;
 
    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 void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }
 
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = in.readInt();
    }
 
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

import java.io.*;
 
/**
 * Created by hollis on 16/2/17.
 */
public class ExternalizableDemo2 {
 
    //爲了便於理解和節省篇幅,忽略關閉流操作及刪除文件操作。真正編碼時千萬不要忘記
    //IOException直接拋出
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //Write Obj to file
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        User2 user = new User2();
        user.setName("hollis");
        user.setAge(23);
        oos.writeObject(user);
        //Read Obj from file
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        User2 newInstance = (User2) ois.readObject();
        //output
        System.out.println(newInstance);
    }
}
//OutPut:
//User{name='hollis', age=23}


這次,就可以把之前的對象狀態持久化下來了。

如果User類中沒有無參數的構造函數,在運行時會拋出異常:java.io.InvalidClassException

更多Externalizable接口使用實例請參考代碼實例

ObjectOutput和ObjectInput 接口

ObjectInput接口 擴展自 DataInput 接口以包含對象的讀操作。

DataInput 接口用於從二進制流中讀取字節,並根據所有 Java 基本類型數據進行重構。同時還提供根據 UTF-8 修改版格式的數據重構 String 的工具。

對於此接口中的所有數據讀取例程來說,如果在讀取所需字節數之前已經到達文件末尾 (end of file),則將拋出 EOFException(IOException 的一種)。如果因爲到達文件末尾以外的其他原因無法讀取字節,則將拋出 IOException 而不是 EOFException。尤其是,在輸入流已關閉的情況下,將拋出 IOException。

ObjectOutput 擴展 DataOutput 接口以包含對象的寫入操作。

DataOutput 接口用於將數據從任意 Java 基本類型轉換爲一系列字節,並將這些字節寫入二進制流。同時還提供了一個將 String 轉換成 UTF-8 修改版格式並寫入所得到的系列字節的工具。

對於此接口中寫入字節的所有方法,如果由於某種原因無法寫入某個字節,則拋出 IOException。

ObjectOutputStream類和ObjectInputStream類

通過前面的代碼片段中我們也能知道,我們一般使用ObjectOutputStream的writeObject方法把一個對象進行持久化。再使用ObjectInputStream的readObject從持久化存儲中把對象讀取出來。

更多關於ObjectInputStream和ObjectOutputStream的相關知識歡迎閱讀我的另外兩篇博文:深入分析Java的序列化與反序列化單例與序列化的那些事兒

Transient 關鍵字

Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。關於Transient 關鍵字的拓展知識歡迎閱讀深入分析Java的序列化與反序列化

序列化ID

虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID)

序列化 ID 在 Eclipse 下提供了兩種生成策略,一個是固定的 1L,一個是隨機生成一個不重複的 long 類型數據(實際上是使用 JDK 工具生成),在這裏有一個建議,如果沒有特殊需求,就是用默認的 1L 就可以,這樣可以確保代碼一致時反序列化成功。那麼隨機生成的序列化 ID 有什麼作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。




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