Java对象序列化详解
下面的文章在公眾號作了更新:點擊查看最新文章
可識別二維碼查看更多最新文章:
寫在前面
Java對象是在JVM中生成的,如果需要遠程傳輸或保存到硬盤上,就需要將Java對象轉換成可傳輸的文件流。
市面上目前有的幾種轉換方式:
- 1. 利用Java的序列化功能序列成字節(字節流)也就是接下來要講的。一般是需要加密傳輸時才用。
- 2. 將對象包裝成JSON字符串(字符流)
轉Json工具有Jackson、FastJson或者GJson,它們各有優缺點:- JackSon:Map、List的轉換可能會出現問題。轉復雜類型的Bean時,轉換的Json格式不是標準的Json格式。適合處理 大文本Json。
- FastJosn:速度最快。將復雜類型的Bean轉換成Json可能會有問題:引用類型如果沒有引用被出錯。適合對性能有要求的場景。
- GJson:功能最全,可以將復雜的Bean和Json字符串進行互轉。性能上面比FastJson有所差距。適合處理小文本Json,和對于數據正確性有要求的場景。
- 3. protoBuf工具(二進制)
性能好,效率高,字節數很小,網絡傳輸節省IO。但二進制格式可讀性差。
一、定義
序列化:把Java對象轉換為字節序列的過程。
反序列化:把字節序列恢復為Java對象的過程。
二、用途
- 把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中;(持久化對象)
- 在網絡上傳送對象的字節序列。(網絡傳輸對象)
Java平臺允許我們在內存中創建可復用的Java對象,但只有當JVM(Java虛擬機)處于運行時,這些對象才可能存在,也就是這些對象的生命周期不會比JVM的生命周期更長。但在現實應用中,就可能要求在JVM停止運行之后能夠保存指定的對象(持久化對象),并在將來重新讀取被保存的對象。
網絡通信時,無論是何種類型的數據,都會轉成字節序列的形式在網絡上傳送。發送方需要把這個Java對象轉換為字節序列,才能在網絡上傳送;接收方則需要把字節序列再恢復為Java對象。
三、實現
實現了如下兩個接口之一的類的對象才能被序列化:
1) Serializable
2) Externalizable
序列化:ObjectOutputStream代表對象輸出流,它的writeObject(Object obj)方法可對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中。
反序化:ObjectInputStream代表對象輸入流,它的readObject()方法從一個源輸入流中讀取字節序列,再把它們反序列化為一個對象,并將其返回。
注:使用writeObject() 和readObject()方法的對象必須已經被序列化
四、serialVersionUID
如果serialVersionUID沒有顯式生成,系統就會自動生成一個。此時,如果在序列化后我們將該類作添加或減少一個字段等的操作,系統在反序列化時會重新生成一個serialVersionUID然后去和已經序列化的對象進行比較,就會報序列號版本不一致的錯誤。為了避免這種問題, 一般系統都會要求實現serialiable接口的類顯式的生明一個serialVersionUID。
所以顯式定義serialVersionUID有如下兩種用途:
1、 希望類的不同版本對序列化兼容時,需要確保類的不同版本具有相同的serialVersionUID;
2、 不希望類的不同版本對序列化兼容時,需要確保類的不同版本具有不同的serialVersionUID。
五、序列化機制算法
1. 所有保存到磁盤中的對象都有一個序列化編號
2. 當程序試圖序列化一個對象時,程序先檢查該對象是否已經被序列化過。如果從未被序列化過,系統就會將該對象轉換成字節序列并輸出;如果已經序列化過,將直接輸出一個序列化編號。
六、示例
要被序列化的對象對應的類的代碼:
public class Person implements Serializable { private String name = null; private Integer age = null; public Person(){System.out.println("無參構造");}public Person(String name, Integer age) { this.name = name; this.age = age; } //getter setter方法省略...@Override public String toString() { return "[" + name + ", " + age+"]"; } }MySerilizable 是一個簡單的序列化程序,它先將一個Person對象保存到文件person.txt中,然后再從該文件中讀出被存儲的Person對象,并打印該對象。
public class MySerilizable {public static void main(String[] args) throws Exception { File file = new File("person.txt"); //序列化持久化對象ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); Person person = new Person("Peter", 27); out.writeObject(person); out.close(); //反序列化,并得到對象ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); Object newPerson = in.readObject(); // 沒有強制轉換到Person類型 in.close(); System.out.println(newPerson); } }輸出結果:
[Peter, 27]結果沒有打印“無參構造”,說明反序列化機制無需通過構造器來初始Java對象。
注:
1.) 反序列化讀取的僅僅是Java對象的數據,而不是Java類,所以在反序列化時必須提供該Java對象所屬類的class文件(這里是Person.class),否則會引發ClassNotFoundException異常。
2).當重新讀取被保存的Person對象時,并沒有調用Person的任何構造器,說明反序列化機制無須通過構造器來初始化對象。
七、選擇序列化
transient
當對某個對象進行序列化時,系統會自動將該對象的所有屬性依次進行序列化,如果某個屬性引用到別一個對象,則被引用的對象也會被序列化。如果被引用的對象的屬性也引用了其他對象,則被引用的對象也會被序列化。 這就是遞歸序列化。
有時候,我們并不希望出現遞歸序列化,或是某個存敏感信息(如銀行密碼)的屬性不被序列化,我們就可通過transient關鍵字修飾該屬性來阻止被序列化。
將上面的Person類的age屬性用transient修飾:
transient private Integer age = null;再去執行MySerilizable的結果為:
[Peter, null] //返序列化時沒有值,說明age字段未被序列化writeObject()方法與readObject()方法
使用transient關鍵字阻止序列化雖然簡單方便,但被它修飾的屬性被完全隔離在序列化機制之外,導致了在反序列化時無法獲取該屬性的值,而通過在需要序列化的對象的Java類里加入writeObject()方法與readObject()方法可以控制如何序列化各屬性,甚至完全不序列化某些屬性(此時就transient一樣)。
如果我們想要上面的Person類里的name屬性在序列化后存在文件里不讓別人知道具體是什么(加密),我們就可在Person類里加如下代碼:
詳細的自定義序列化與反序列化可參見ObjectOutputStream 和ObjectInputStream 類的JDK文檔。
Externalizable接口
Externalizable接口 與Serializable 接口類似,只是Externalizable接口需要強制自定義序列化。
要序列化對象的代碼:
主函數代碼改為:
public class MySerilizable {public static void main(String[] args) throws Exception { File file = new File("person.txt"); //序列化持久化對象ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); Teacher person = new Teacher("Peter", 27); out.writeObject(person); out.close(); //反序列化,并得到對象ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); Object newPerson = in.readObject(); // 沒有強制轉換到Person類型 in.close(); System.out.println(newPerson); } }打印結果:
有參構造 無參構造 //與Serializable 不同的是,還調用了無參構造 [Peter, null] //age未被序列化,所以未取到值八、單例模式的序列化
當我們使用Singleton模式時,應該是期望某個類的實例應該是唯一的,但如果該類是可序列化的,那么情況可能略有不同。對前面使用的Person類進行修改,使其實現Singleton模式,如下所示:
public class Person implements Serializable { private static class InstanceHolder { private static final Person instatnce = new Person("John", 31, "男"); } public static Person getInstance() { return InstanceHolder.instatnce; } private String name = null; private Integer age = null; private String gender = null; private Person() { System.out.println("必須私有化的無參構造"); } private Person(String name, Integer age, String gender) { System.out.println("有參構造"); this.name = name; this.age = age; this.gender = gender; } ... }同時要修改MySerilizable 應用,使得能夠保存/獲取上述單例對象,并進行對象相等性比較,如下代碼所示:
public class MySerilizable { public static void main(String[] args) throws Exception { File file = new File("person.txt"); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); out.writeObject(Person.getInstance()); // 保存單例對象 out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); Object newPerson = in.readObject(); in.close(); System.out.println(newPerson); System.out.println(Person.getInstance() == newPerson); // 將獲取的對象與Person類中的單例對象進行相等性比較 } }打印結果:
有參構造 [John, 31, 男] false //說明不是同一個對象九、序列化對象注意事項
總結
以上是生活随笔為你收集整理的Java对象序列化详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Servlet3.1 新增的非阻塞式IO
- 下一篇: 字符串的快速匹配