Java对象的序列化与反序列化
序列化與反序列化
序列化 (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。
雖然Serializable接口中并沒有定義任何屬性和方法,但是如果一個類想要具備序列化能力也必須要實現它。其實,主要是因為序列化在真正的執行過程中會使用instanceof判斷一個類是否實現接口Serializable,如果未實現則直接拋出異常。關于這部分內容,我會單開一篇文章講解。
如果要序列化的類有父類,要想同時將在父類中定義過的變量持久化下來,那么父類也應該繼承java.io.Serializable接口。
下面是一個實現了java.io.Serializable接口的類
package?com.hollischaung.serialization.SerializableDemos; import?java.io.Serializable; /** * Created by hollis on 16/2/17. * 實現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;}@Overridepublic?String?toString()?{return?"User{"?+"name='"?+ name +?'\''?+", age="?+ age +'}';} }通過下面的代碼進行序列化及反序列化
package com.hollischaung.serialization.SerializableDemos;import?java.io.File; import?java.io.FileInputStream; import?java.io.FileOutputStream; import?java.io.IOException; import?java.io.ObjectInputStream; import?java.io.ObjectOutputStream;/** * Created by hollis on 16/2/17. * SerializableDemo1 結合SerializableDemo2說明 一個類要想被序列化必須實現Serializable接口 */ public?class?SerializableDemo1?{public?static?void?main(String[] args)?{//Initializes The ObjectUser1 user =?new?User1();user.setName("hollis");user.setAge(23);System.out.println(user);//Write Obj to Filetry?(FileOutputStream fos =?new?FileOutputStream("tempFile"); ObjectOutputStream oos =?new?ObjectOutputStream(fos)) {oos.writeObject(user);}?catch?(IOException e) {e.printStackTrace();}//Read Obj from FileFile file =?new?File("tempFile");try?(ObjectInputStream ois =?new?ObjectInputStream(new?FileInputStream(file))) {User1 newUser = (User1)ois.readObject();System.out.println(newUser);}?catch?(IOException | ClassNotFoundException e) {e.printStackTrace();}} }//OutPut: //User{name='hollis', age=23} //User{name='hollis', age=23}如果你觀察夠細微的話,你可能會發現,我在上面的測試代碼中使用了IO流,但是我并沒有顯式的關閉它。這其實是Java 7中的新特性try-with-resources。這其實是Java中的一個語法糖,背后原理其實是編譯器幫我們做了關閉IO流的工作。后面我會單獨出一篇文章介紹下如何使用語法糖提高代碼質量。
上面的代碼中,我們將代碼中定義出來的User對象通過序列化的方式保存到文件中,然后再從文件中將它反序列化成Java對象。結果是我們的對象的屬性均被持久化了下來。
Externalizable接口
除了Serializable 之外,java中還提供了另一個序列化接口Externalizable
為了了解Externalizable接口和Serializable接口的區別,先來看代碼,我們把上面的代碼改成使用Externalizable的形式。
package?com.hollischaung.serialization.ExternalizableDemos;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?{}@Overridepublic?String?toString()?{return?"User{"?+"name='"?+ name +?'\''?+", age="?+ age +'}';} }?
package com.hollischaung.serialization.ExternalizableDemos;import?java.io.*;/** * Created by hollis on 16/2/17. * 對一個實現了Externalizable接口的類進行序列化及反序列化 */ public?class?ExternalizableDemo1?{public?static?void?main(String[] args)?{//Write Obj to fileUser1 user =?new?User1();user.setName("hollis");user.setAge(23);try(ObjectOutputStream oos =?new?ObjectOutputStream(new?FileOutputStream("tempFile"))){oos.writeObject(user);}?catch?(IOException e) {e.printStackTrace();}//Read Obj from fileFile file =?new?File("tempFile");try(ObjectInputStream ois = ?new?ObjectInputStream(new?FileInputStream(file))){User1 newInstance = (User1) ois.readObject();//outputSystem.out.println(newInstance);}?catch?(IOException | ClassNotFoundException e ) {e.printStackTrace();}} } //OutPut: //User{name='null', age=0}通過上面的實例的輸出結果可以發現,對User1類進行序列化及反序列化之后得到的對象的所有屬性的值都變成了默認值。也就是說,之前的那個對象的狀態并沒有被持久化下來。這就是Externalizable接口和Serializable接口的區別:
Externalizable繼承了Serializable,該接口中定義了兩個抽象方法:writeExternal()與readExternal()。當使用Externalizable接口來進行序列化與反序列化的時候需要開發人員重寫writeExternal()與readExternal()方法。
由于上面的代碼中,并沒有在這兩個方法中定義序列化實現細節,所以輸出的內容為空。還有一點值得注意:在使用Externalizable進行序列化的時候,在讀取對象時,會調用被序列化類的無參構造器去創建一個新的對象,然后再將被保存對象的字段的值分別填充到新對象中。所以,實現Externalizable接口的類必須要提供一個public的無參的構造器。
如果實現了Externalizable接口的類中沒有無參數的構造函數,在運行時會拋出異常:java.io.InvalidClassException。如果一個Java類沒有定義任何構造函數,編譯器會幫我們自動添加一個無參的構造方法,可是,如果我們在類中定義了一個有參數的構造方法了,編譯器便不會再幫我們創建無參構造方法,這點需要注意。
按照要求修改之后代碼如下:
package?com.hollischaung.serialization.ExternalizableDemos;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();}@Overridepublic?String?toString()?{return?"User{"?+"name='"?+ name +?'\''?+", age="?+ age +'}';} }再執行測試得到以下結果
//OutPut: //User{name='hollis', age=23}這次,就可以把之前的對象狀態持久化下來了。
ObjectOutput和ObjectInput 接口
上面的writeExternal方法和readExternal方法分別接收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的相關知識,我會單獨有一篇文章介紹,敬請期待。
?
transient 關鍵字
transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設為初始值,如 int 型的是 0,對象型的是 null。關于transient 關鍵字的拓展同樣下一篇文章介紹。
?
序列化ID
虛擬機是否允許反序列化,不僅取決于類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是?private static final long serialVersionUID)
序列化 ID 在 Eclipse 下提供了兩種生成策略,一個是固定的 1L,一個是隨機生成一個不重復的 long 類型數據(實際上是使用 JDK 工具生成),在這里有一個建議,如果沒有特殊需求,就是用默認的 1L 就可以,這樣可以確保代碼一致時反序列化成功。那么隨機生成的序列化 ID 有什么作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的Java对象的序列化与反序列化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一次毕生难忘的 Java 内存泄漏排查经
- 下一篇: 为什么阿里巴巴禁止开发人员使用isSuc