細看Java序列化機制

概況

在程序中爲了能直接以 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 方法這裏須要說明下,在對某個對象進行寫入時,它其實不只僅序列化本身,還會去遍歷尋找相關引用的其餘對象,由本身和其餘引用對象組成的一個完整的對象圖關係都會被序列化。bash

對於數組、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 接口。ui

另外,若是父類未實現 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();
複製代碼

-------------推薦閱讀------------

從JDK角度認識枚舉enum

個人2017文章彙總——機器學習篇

個人2017文章彙總——Java及中間件

個人2017文章彙總——深度學習篇

個人2017文章彙總——JDK源碼篇

個人2017文章彙總——天然語言處理篇

個人2017文章彙總——Java併發篇

------------------廣告時間----------------

公衆號的菜單已分爲「分佈式」、「機器學習」、「深度學習」、「NLP」、「Java深度」、「Java併發核心」、「JDK源碼」、「Tomcat內核」等,可能有一款適合你的胃口。

鄙人的新書《Tomcat內核設計剖析》已經在京東銷售了,有須要的朋友能夠購買。感謝各位朋友。

爲何寫《Tomcat內核設計剖析》

歡迎關注:

這裏寫圖片描述
相關文章
相關標籤/搜索