Java容許咱們在內存中建立可複用的Java對象,但通常狀況下,這些對象的生命週期不會比JVM的生命週期更長。但在現實應用中,可能要求在JVM中止運行以後可以保存(持久化)指定的對象,並在未來從新讀取被保存的對象html
Java對象序列化就可以幫助咱們實現該功能。使用Java對象序列化,在保存對象時,會把其狀態保存爲一組字節,在將來再將這些字節組裝成對象java
必須注意地是,對象序列化保存的是對象的"狀態",即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量程序員
除了在持久化對象時會用到對象序列化以外,當使用 RMI ,或在網絡中傳遞對象時,都會用到對象序列化設計模式
Java序列化API爲處理對象序列化提供了一個標準機制,該API簡單易用,但性能不是最好的數組
在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()方法便可序列化一個對象ide
反序列化的過程須要創造InputStream對象(如FileInputstream、ByteArrayInputStream等),而後將其封裝在ObjectInputStream中,在調用readObject()便可函數
注意對象的序列化是基於字節的,不能使用基於字符的流。
使用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 修飾,默認序列化機制就會忽略該字段。此處將Person類中的age字段聲明爲transient,以下所示
public class Person implements Serializable { ... transient private Integer age = null; ... } // 再執行SimpleSerial應用程序,會有以下輸出: arg constructor [John, null, MALE]
對於上述已被聲明爲 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方法。這兩個方法會在序列化、反序列化的過程當中被自動調用。且不能關閉流,不然會致使序列化操做失敗。
不管是使用 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]
當使用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() 方法,這個方法的做用是清除流中保存的寫入對象的記錄
安裝 serialVersionUID 插件便可。
參考 Stack Overflow:https://stackoverflow.com/questions/285793/what-is-a-serialversionuid-and-why-should-i-use-it
ArrayList實現了java.io.Serializable接口,可是其 elementData 是 transient 的,可是 ArrayList 是經過數組實現的,數組 elementData 用來保存列表中的元素。經過該屬性的聲明方式知道該數據沒法經過序列化持久化。
可是若是實際測試,就會發現,ArrayList 能被完整的序列化,緣由是在writeObject 和 readObject方法中進行了序列化的實現。
這樣設計的緣由是由於 ArrayList 是動態數組,若是數組自動增加長度設爲 2000,而實際只放了一個元素,那就會序列化 1999 個 null 元素,爲了保證在序列化的時候不會將這麼多 null 元素序列化,ArrayList 把元素數組設置爲transient,可是,做爲一個集合,在序列化過程當中還必須保證其中的元素能夠被持久化,因此,經過重寫 writeObject 和 readObject 方法把其中的元素保留下來,具體作法是:
writeObject方法把elementData數組中的元素遍歷到ObjectOutputStream