細看Java序列化機制

概況

在程序中為了能直接以 Java 對象的形式進行保存,然後再重新得到該 Java 對象,這就需要序列化能力。序列化其實可以看成是一種機制,按照一定的格式將 Java 對象的某狀態轉成介質可接受的形式,以方便存儲或傳輸。其實想想就大致清楚基本流程,序列化時將 Java 對象相關的類信息、屬性及屬性值等等保存起來,反序列化時再根據這些信息構建出 Java 對象。而過程可能涉及到其他對象的引用,所以這裡引用的對象的相關信息也要參與序列化。

Java 中進行序列化操作需要實現 Serializable 或 Externalizable 接口。

序列化的作用

  • 提供一種簡單又可擴展的對象保存恢復機制。

  • 對於遠程調用,能方便對對象進行編碼和解碼,就像實現對象直接傳輸。

  • 可以將對象持久化到介質中,就像實現對象直接存儲。

  • 允許對象自定義外部存儲的格式。

序列化例子

FileOutputStream f = new FileOutputStream("tmp.o");ObjectOutput s = new ObjectOutputStream(f);s.writeObject("test");s.writeObject(new ArrayList());s.flush();

常見的使用方式是直接將對象寫入流中,比如上述例子中,創建了 FileOutputStream 對象,其對應輸出到 tmp.o 文件中,然後創建 ObjectOutputStream 對象嵌套前面的輸出流。當我們調用 writeObject 方法時即能進行序列化操作。

writeObject 方法這裡需要說明下,在對某個對象進行寫入時,它其實不僅僅序列化自己,還會去遍歷尋找相關引用的其他對象,由自己和其他引用對象組成的一個完整的對象圖關係都會被序列化。

對於數組、enum、Class類對象、ObjectStreamClass 和 String 等都會做特殊處理,而其他對象序列化則需要實現 Serializable 或 Externalizable 接口。

反序列化例子

FileInputStream in = new FileInputStream("tmp.o");ObjectInputStream s = new ObjectInputStream(in);String test = (String)s.readObject();List list = (ArrayList)s.readObject();

針對序列化則存在反序列化操作,通過流直接讀取對象,先創建 FileInputStream 對象,其對應輸入文件為 tmp.o,然後創建 ObjectInputStream 對象嵌套前面的輸入流,接著則可以調用 readObject 方法讀取對象。

其中調用 readObject 方法反序列操作的過程,除了會恢復對象自己之外還會遍歷整個完整的對象圖,創建整個對象圖包含的所有對象。

serialVersionUID 有什麼用

在序列化操作時,經常會看到實現了 Serializable 接口的類會存在一個 serialVersionUID 屬性,並且它是一個固定數值的靜態變量。比如如下,這個屬性有什麼作用?其實它主要用於驗證版本一致性,每個類都擁有這麼一個 ID,在序列化的時候會一起被寫入流中,那麼在反序列化的時候就被拿出來跟當前類的 serialVersionUID 值進行比較,兩者相同則說明版本一致,可以序列化成功,而如果不同則序列化失敗。

private static final long serialVersionUID = -6849794470754667710L;

一般情況下我們可以自己定義 serialVersionUID 的值或者 IDE 幫我們自動生成,而如果我們不顯示定義 serialVersionUID 的話,這不代表不存在 serialVersionUID,而是由 JDK 幫我們生成,生成規則是會利用類名、類修飾符、接口名、字段、靜態初始化信息、構造函數信息、方法名、方法修飾符、方法簽名等組成的信息,經過 SHA 算法生成摘要即是最終的 serialVersionUID 值。

父類序列化什麼情況

如果一個子類實現了 Serializable 接口而父類沒有實現該接口,則在序列化子類時,子類的屬性狀態會被寫入而父類的屬性狀態將不被寫入。所以如果想要父類屬性狀態也一起參與序列化,就要讓它也實現 Serializable 接口。

另外,如果父類未實現 Serializable 接口則反序列化生成的對象會再次調用父類的構造函數,以此完成對父類的初始化。所以父類屬性初始值一般都是類型的默認值。比如下面,Father 類的屬性不會參與序列化,反序列化時 Father 對象的屬性的值為默認值0。

public class Father { public int f; public Father() { }}public class Son extends Father implements Serializable { public int s; public Son() { super(); }}

哪些字段會序列化

在序列化時類的哪些字段會參與到序列化中呢?其實有兩種方式決定哪些字段會被序列化,

  1. 默認方式,Java對象中的非靜態和非transient的字段都會被定義為需要序列的字段。

  2. 另外一種方式是通過 ObjectStreamField 數組來聲明類需要序列化的對象。

可以看到普通的字段都是默認會被序列化的,而對於某些包含敏感信息的字段我們不希望它參與序列化,那麼最簡單的方式就是可以將該字段聲明為 transient。

如何使用 ObjectStreamField?舉個例子,如下,A類中有 name 和 password 兩個字段,通過 ObjectStreamField 數組聲明只序列化 name 字段。這種聲明的方式不用糾結為什麼這樣,這僅僅是約定了這樣而已。

public class A implements Serializable { String name; String password private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("name", String.class)};}

枚舉類型的序列化

Enum 類型的序列化與普通的 Java 類的序列化有所不同,那麼在深入之前可以先看這篇文章深入瞭解下枚舉,《 從JDK角度認識枚舉enum》。

所以我們知道枚舉被編譯後會變成一個繼承 java.lang.Enum 的類,而且枚舉裡面的元素被聲明成 static final ,另外生成一個靜態代碼塊 static{},最後還會生成 values 和 valueOf 兩個方法。Enum 類是一個抽象類,主要有 name 和 ordinal 兩個屬性,分別用於表示枚舉元素的名稱和枚舉元素的位置索引。

Enum 類型參與序列化時只會將枚舉對象中的 name 屬性寫入,而其他的屬性則不參與進來。在反序列化時,則是先讀取 name 屬性,然後再通過 java.lang.Enum 類的 valueOf 方法找到對應的枚舉類型。

除此之外,不能自定義 Enum 類型的序列化,所以 writeObject, readObject, readObjectNoData, writeReplace 以及 readResolve 等方法在序列化時會被忽略,類似的,serialPersistentFields 和 serialVersionUID 屬性都會被忽略。

最後,在序列化場景中,涉及到使用枚舉的情況時要仔細設計好,不然很可能會因為後面升級修改了枚舉類的結構而導致反序列化失敗。

Externalizable 接口作用

Externalizable 接口主要就是提供給用戶自己控制序列化內容,雖然前面我們也看到了 transient 和 ObjectStreamField 能定義序列化的字段,但通過 Externalizable 接口則能更加靈活。可以看到它其實繼承了 Serializable 接口,提供了 writeExternal 和 readExternal 兩個方法,也就是在這兩個方法內控制序列化和反序列化的內容。

public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;}

比如下面的例子,我們可以在 writeExternal 方法中額外寫入 Date 對象,然後再寫入 value 值。對應的,反序列化時則是在 readExternal 方法中讀取 Date 對象和 value。這樣就完成了自定義序列化操作。

public class ExternalizableTest implements Externalizable { public String value = "test"; public ExternalizableTest() { } public void writeExternal(ObjectOutput out) throws IOException { Date d = new Date(); out.writeObject(d); out.writeObject(value); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { Date d = (Date) in.readObject(); System.out.println(d); System.out.println((String) in.readObject()); }}

寫入時替換對象

正常情況下序列化某個對象時寫入的正是當前的對象,但如果說我們要替換當前的對象而寫入其他對象的話則可以通過 writeReplace 方法來實現。比如下面,person 類通過 writeReplace 方法最終可以寫入 Object 數組對象。所以我們在反序列化時就不再是轉換成 Person 類型,而是要轉換為 Object 數組對象。

class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } private Object writeReplace() throws ObjectStreamException { Object[] properties = new Object[2]; properties[0] = name; properties[1] = age; return properties; }}
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.o"));Object[] properties = (Object[]) ois.readObject();

讀取時替換對象

上面介紹了在寫入時可以替換對象,而在讀取時也同樣支持替換對象的,它是通過 readResolve 方法實現的。比如下面,在 readResolve 方法返回 2222,則反序列化讀取時不再是 Person 對象,而是 2222。

class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } private Object readResolve() throws ObjectStreamException { return 2222; }}
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.o"));Object o = ois.readObject();


分享到:


相關文章: