前言
Java允許我們在內存中創建可復用的Java對象,但一般情況下,這些對象的生命周期不會比JVM的生命周期更長。但在現實應用中,可能要求在JVM停止運行之后能夠保存(持久化)指定的對象,並在將來重新讀取被保存的對象
Java對象序列化就能夠幫助我們實現該功能。使用Java對象序列化,在保存對象時,會把其狀態保存為一組字節,在未來再將這些字節組裝成對象
必須注意地是,對象序列化保存的是對象的"狀態",即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量
除了在持久化對象時會用到對象序列化之外,當使用 RMI ,或在網絡中傳遞對象時,都會用到對象序列化
Java序列化API為處理對象序列化提供了一個標准機制,該API簡單易用,但性能不是最好的
實例 demo
在Java中,只要一個類實現了 java.io.Serializable 接口,它就可以被序列化(枚舉類可以被序列化)。
// Gender類,表示性別 // 每個枚舉類型都會默認繼承類java.lang.Enum,而Enum類實現了Serializable接口,所以枚舉類型對象都是默認可以被序列化的。 public enum Gender { MALE, FEMALE } // Person 類實現了 Serializable 接口,它包含三個字段。另外,它還重寫了該類的 toString() 方法,以方便打印 Person 實例中的內容。 public class Person implements Serializable { private String name = null; private Integer age = null; private Gender gender = null; public Person() { System.out.println("none-arg constructor"); } public Person(String name, Integer age, Gender gender) { System.out.println("arg constructor"); this.name = name; this.age = age; this.gender = gender; } // 省略 set get 方法
@Override public String toString() { return "[" + name + ", " + age + ", " + gender + "]"; } } // SimpleSerial類,是一個簡單的序列化程序,它先將Person對象保存到文件person.out中,然后再從該文件中讀出被存儲的Person對象,並打印該對象。 public class SimpleSerial { public static void main(String[] args) throws Exception { File file = new File("person.out"); ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file)); // 注意這里使用的是 ObjectOutputStream 對象輸出流封裝其他的輸出流 Person person = new Person("John", 101, Gender.MALE); oout.writeObject(person); oout.close(); ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); // 使用對象輸入流讀取序列化的對象 Object newPerson = oin.readObject(); // 沒有強制轉換到Person類型 oin.close(); System.out.println(newPerson); } } // 上述程序的輸出的結果為: arg constructor [John, 31, MALE]
當重新讀取被保存的Person對象時,並沒有調用Person的任何構造器,看起來就像是直接使用字節將Person對象還原出來的。當Person對象被保存到person.out文件后,可以在其它地方去讀取該文件以還原對象,但必須確保該讀取程序的 CLASSPATH 中包含有 Person.class(哪怕在讀取Person對象時並沒有顯示地使用Person類,如上例所示),否則會拋出 ClassNotFoundException。
簡單的來說,Java 對象序列化就是把對象寫入到輸出流中,用來存儲或傳輸;反序列化就是從輸入流中讀取對象。
序列化一個對象首先要創造某些OutputStream對象(如FileOutputStream、ByteArrayOutputStream等),然后將其封裝在一個ObjectOutputStream對象中,在調用writeObject()方法即可序列化一個對象
反序列化的過程需要創造InputStream對象(如FileInputstream、ByteArrayInputStream等),然后將其封裝在ObjectInputStream中,在調用readObject()即可
注意對象的序列化是基於字節的,不能使用基於字符的流。
為什么一個類實現了Serializable接口,它就可以被序列化?
使用ObjectOutputStream來持久化對象到文件中,使用了writeObject方法,該方法又調用了如下方法:
private void writeObject0(Object obj, boolean unshared) throws IOException { ... if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException(cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } ... }
從上述代碼可知,如果被寫對象的類型是String,或數組,或Enum,或Serializable,那么就可以對該對象進行序列化,否則將拋出NotSerializableException。
即、String類型的對象、枚舉類型的對象、數組對象,都是默認可以被序列化的。
默認序列化機制
如果僅僅讓某個類實現Serializable接口,而沒有其它任何處理的話,則就是使用默認序列化機制。
使用默認機制在序列化對象時,不僅會序列化當前對象,還會對該對象引用的其它對象也進行序列化,同樣地,這些其它對象引用的另外對象也將被序列化,以此類推。
所以,如果一個對象包含的成員變量是容器類對象,而這些容器所含有的元素也是容器類對象,那么這個序列化的過程就會較復雜,開銷也較大。
選擇性的序列化
在現實應用中,有些時候不能使用默認序列化機制。比如,希望在序列化過程中忽略掉敏感數據,或者簡化序列化過程。下面將介紹若干影響序列化的方法。
使用 transient 關鍵字
當類的某個字段被 transient 修飾,默認序列化機制就會忽略該字段。此處將Person類中的age字段聲明為transient,如下所示
public class Person implements Serializable { ... transient private Integer age = null; ... } // 再執行SimpleSerial應用程序,會有如下輸出: arg constructor [John, null, MALE]
使用writeObject()方法與readObject()方法
對於上述已被聲明為 transitive 的字段 age,除了將 transient 關鍵字去掉外,是否還有其它方法能使它再次可被序列化?
方法之一就是在Person類中添加兩個方法:writeObject()與readObject(),如下所示:
public class Person implements Serializable { ... transient private Integer age = null; ... // writeObject()會先調用ObjectOutputStream中的defaultWriteObject()方法,該方法會執行默認的序列化機制,此時會忽略掉age字段。然后再調用writeInt()方法顯示地將age字段寫入到 // ObjectOutputStream中。 private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeInt(age); } // readObject()的作用則是針對對象的讀取,其原理與writeObject()方法相同。 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); age = in.readInt(); } } // 再次執行SimpleSerial應用程序,則又會有如下輸出: arg constructor [John, 31, MALE]
必須注意地是,writeObject()與readObject()都是private方法,那么它們是如何被調用的呢?
毫無疑問,使用反射。詳情可以看看ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。這兩個方法會在序列化、反序列化的過程中被自動調用。且不能關閉流,否則會導致序列化操作失敗。
使用 Externalizable 接口
無論是使用 transient 關鍵字,還是使用 writeObject() 和 readObject() 方法,其實都是基於 Serializable 接口的序列化。
Java提供了另一個序列化接口 Externalizable,使用該接口之后,之前基於 Serializable 接口的序列化機制就將失效。Externalizable 接口繼承於 Serializable 接口,當使用該接口時,序列化的細節需要由程序員去完成。將Person類作如下修改:
public class Person implements Externalizable { private String name = null; transient private Integer age = null; private Gender gender = null; public Person() { System.out.println("none-arg constructor"); } public Person(String name, Integer age, Gender gender) { System.out.println("arg constructor"); this.name = name; this.age = age; this.gender = gender; } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeInt(age); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); age = in.readInt(); } @Override public void writeExternal(ObjectOutput out) throws IOException { } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { } ... } // 此時再執行SimpleSerial程序,會得到如下結果: arg constructor none-arg constructor [null, null, null] // 從該結果,一方面可以看出Person對象中任何一個字段都沒有被序列化。另一方面,這次序列化過程調用了Person類的無參構造器。
Externalizable 繼承於 Serializable,當使用該接口時,序列化的細節需要由程序員去完成。
如上所示的代碼,由於實現的writeExternal()與readExternal()方法未作任何處理,那么該序列化行為將不會保存/讀取任何一個字段。這也就是為什么輸出結果中所有字段的值均為空。
另外,使用 Externalizable 接口進行序列化時,讀取對象會調用被序列化類的無參構造器去創建一個新的對象,然后再將被保存對象的字段的值分別填充到新對象中,這就是為什么在此次序列化過程中Person類的無參構造器會被調用。由於這個原因,實現 Externalizable 接口的類必須要提供一個無參構造器,且它的訪問權限為public。
對上述Person類做進一步的修改,使其能夠對name與age字段進行序列化,但忽略 gender 字段:
public class Person implements Externalizable { private String name = null; transient private Integer age = null; private Gender gender = null; public Person() { System.out.println("none-arg constructor"); } public Person(String name, Integer age, Gender gender) { System.out.println("arg constructor"); this.name = name; this.age = age; this.gender = gender; } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeInt(age); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); age = in.readInt(); } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); age = in.readInt(); } ... } // 執行SimpleSerial之后會有如下結果: arg constructor none-arg constructor [John, 31, null]
readResolve()方法——單例模式的反序列化
當使用Singleton模式時,應該是期望某個類的實例應該是唯一的,但如果該類是可序列化的,那么情況可能略有不同。當然目前最好的單例實現方式是使用枚舉,如果還是傳統的實現方式,才會遇到這個問題。
具體參考:最簡單的設計模式——單例模式的演進和推薦寫法(Java 版)
序列化和反序列化需要注意的坑
能序列化的前提
如果一個類想被序列化,需要實現 Serializable 接口進行自動序列化,或者實現 Externalizable 接口進行手動序列化,否則強行序列化該類的對象,就會拋出 NotSerializableException 異常,這是因為,在序列化操作過程中會對類型進行檢查,要求被序列化的類必須屬於 Enum、Array 和 Serializable 類型其中的任何一種(Externalizable也繼承了Serializable)。
JVM 是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID)
transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設為初始值,如 int 型的是 0,對象型的是 null。
FileOutputStream 類有一個帶有兩個參數的重載 Constructor——FileOutputStream(String, boolean)。若其第二個參數為 true 且 String 代表的文件存在,那么將把新的內容寫到原來文件的末尾而非重寫這個文件,故不能用這個版本的構造函數來實現序列化,也就是說必須重寫這個文件,否則在讀取這個文件反序列化的過程中就會拋出異常,導致只有第一次寫到這個文件中的對象可以被反序列化,之后程序就會出錯。
要知道序列化的是什么樣兒的對象(成員)
序列化並不保存靜態變量
要想將父類對象也序列化,就需要讓父類也實現 Serializable 接口
若一個類的字段有引用對象,那么在序列化該類的時候不僅該類要實現Serializable接口,這個引用類型也要實現Serializable接口。但有時我們並不需要對這個引用類型進行序列化,此時就需要使用transient關鍵字來修飾該引用類型保證在序列化的過程中跳過該引用類型。
通過序列化操作,可以實現對任何可 Serializable 對象的深度復制(deep copy),這意味着復制的是整個對象的關系網,而不僅僅是基本對象及其引用
如果父類沒有實現Serializable接口,但其子類實現了此接口,那么這個子類是可以序列化的,但是在反序列化的過程中會調用父類的無參構造函數,所以在其直接父類(注意是直接父類)中必須有一個無參的構造函數。
序列化的安全性
服務器端給客戶端發送序列化對象數據,序列化二進制格式的數據寫在文檔中,並且完全可逆。一抓包就能就看到類是什么樣子,以及它包含什么內容。如果對象中有一些數據是敏感的,比如密碼字符串等,則要對字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,才可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。
比如可以通過使用 writeObject 和 readObject 實現密碼加密和簽名管理,但其實還有更好的方式。
如果需要對整個對象進行加密和簽名,最簡單的是將它放在一個 javax.crypto.SealedObject 和/或 java.security.SignedObject 包裝器中。兩者都是可序列化的,所以將對象包裝在 SealedObject 中可以圍繞原對象創建一種 “包裝盒”。必須有對稱密鑰才能解密,而且密鑰必須單獨管理。同樣,也可以將 SignedObject 用於數據驗證,並且對稱密鑰也必須單獨管理
反序列化后,何時不是同一個對象
只要將對象序列化到單一流中,就可以恢復出與我們寫出時一樣的對象網,而且只要在同一流中,對象都是同一個。否則,反序列化后的對象地址和原對象地址不同,只是內容相同
如果將一個對象序列化入某文件,那么之后又對這個對象進行修改,然后再把修改的對象重新寫入該文件,那么修改無效,文件保存的序列化的對象仍然是最原始的。這是因為,序列化輸出過程跟蹤了寫入流的對象,而試圖將同一個對象寫入流時,並不會導致該對象被復制,而只是將一個句柄寫入流,該句柄指向流中相同對象的第一個對象出現的位置。為了避免這種情況,在后續的 writeObject() 之前調用 out.reset() 方法,這個方法的作用是清除流中保存的寫入對象的記錄
idea IDE 自動生成序列化版本號
安裝 serialVersionUID 插件即可。
序列化版本號的用處
參考 Stack Overflow:https://stackoverflow.com/questions/285793/what-is-a-serialversionuid-and-why-should-i-use-it
ArrayList 序列化要注意的問題
ArrayList實現了java.io.Serializable接口,但是其 elementData 是 transient 的,但是 ArrayList 是通過數組實現的,數組 elementData 用來保存列表中的元素。通過該屬性的聲明方式知道該數據無法通過序列化持久化。
但是如果實際測試,就會發現,ArrayList 能被完整的序列化,原因是在writeObject 和 readObject方法中進行了序列化的實現。
這樣設計的原因是因為 ArrayList 是動態數組,如果數組自動增長長度設為 2000,而實際只放了一個元素,那就會序列化 1999 個 null 元素,為了保證在序列化的時候不會將這么多 null 元素序列化,ArrayList 把元素數組設置為transient,但是,作為一個集合,在序列化過程中還必須保證其中的元素可以被持久化,所以,通過重寫 writeObject 和 readObject 方法把其中的元素保留下來,具體做法是:
writeObject方法把elementData數組中的元素遍歷到ObjectOutputStream
歡迎關注
dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!