Java基礎系列-序列化與反序列化
原創文章,轉載請標註出處:《Java基礎系列-序列化與反序列化》
一、序列化簡介
在專案中有很多情況需要對例項物件進行序列化與反序列化,這樣可以持久的儲存物件的狀態,甚至在各個元件之間進行物件傳遞和遠端呼叫。序列化機制是專案中必不可少的常用機制。
要想一個類擁有序列化、反序列化功能,最簡單的方法就是實現java.io.Serializable介面,這個介面是一個標記介面(marker Interface),即其內部無任何欄位與方法定義。
當我們定義了一個實現Serializable介面的類之後,一般我們會手動在類內部定義一個private static final long serialVersionUID欄位,用來儲存當前類的序列版本號。這樣做的目的就是唯一標識該類,其物件持久化之後這個欄位將會儲存到持久化檔案中,當我們對這個類做了一些更改時,新的更改可以根據這個版本號找到已持久化的內容,來保證來自類的更改能夠準確的體現到持久化內容中。而不至於因為未定義版本號,而找不到原持久化內容。
當然如果我們不實現Serializable介面就對該類進行序列化與反序列化操作,那麼將會丟擲java.io.NotSerializableException異常。
如下例子:
public class Student implements Serializable { private static final long serialVersionUID = -3111843137944176097L; private String name; private int age; private String sex; private String address; private String phone; 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 String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } }
二、序列化的使用
雖然要實現序列化只需要實現Serializable介面即可,但這只是讓類的物件擁有可被序列化和反序列化的功能,它自己並不會自動實現序列化與反序列化,我們需要編寫程式碼來進行序列化與反序列化。
這就需要使用ObjectOutputStream類的writeObject()方法與readObject()方法,這兩個方法分別對應於將物件寫入到流中(序列化),從流中讀取物件(反序列化)。
Java中的物件序列化,序列化的是什麼?答案是物件的狀態、更具體的說就是物件中的欄位及其值,因為這些值正好描述了物件的狀態。
下面的例子我們實現將Student類的一個例項持久化到本地檔案“D:/student.out”中,並從本地檔案中讀到記憶體,這要藉助於FileOutputStream和FileInputStream來實現:
public class SerilizeTest { public static void main(String[] args) { serilize(); Student s = (Student) deserilize(); System.out.println("姓名:" + s.getName()+"\n年齡:"+ s.getAge()+"\n性別:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手機:"+s.getPhone()); } public static Object deserilize(){ Student s = new Student(); InputStream is = null; ObjectInputStream ois = null; File f = new File("D:/student.out"); try { is = new FileInputStream(f); ois = new ObjectInputStream(is); s = (Student)ois.readObject(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); }finally{ if(ois != null){ try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } if(is != null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return s; } public static void serilize() { Student s = new Student(); s.setName("張三"); s.setAge(32); s.setSex("man"); s.setAddress("北京"); s.setPhone("12345678910"); //s.setPassword("123456"); OutputStream os = null; ObjectOutputStream oos = null; File f = new File("D:/student.out"); try { os = new FileOutputStream(f); oos = new ObjectOutputStream(os); oos.writeObject(s); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally{ if(oos != null) try { oos.close(); } catch (IOException e) { e.printStackTrace(); } if(os != null) try { os.close(); } catch (IOException e) { e.printStackTrace(); } } } }
通過以上的程式碼就可以實現簡單的物件序列化與反序列化。
執行結果:
姓名:張三 年齡:32 性別:man 地址:北京 手機:12345678910
這裡將writeObject的呼叫棧羅列出來:
writeObject->writeObject0->writeOrdinaryObject->writeSerialData->defaultWriteFields->writeObject0->...
呼叫棧最後返回了writeObject0方法,這是使用遞迴的方式來遍歷目標類的欄位中所有普通實現Serializable介面的型別欄位,將其全部寫入流中,最後所有的寫入都會在writeObject0方法中終結,這個方法會根據欄位的型別來呼叫響應的write方法進行流寫入。
Java序列化的是物件的欄位,但是這些欄位並不一定都是簡單的String、或者是Integer之類,可能也是很複雜的型別,一個實現了Serializable介面的類型別,這時候我們序列化的時候,就需要將這個內部的第二層次的物件進行遞迴序列化,這種巢狀可以有無數層,但是總會有個終結。
三、自定義序列化功能
上面的內容都是簡單又簡單,真正要注意的內容在這裡,有關自定義序列化策略的內容才是序列化機制中最重要、最複雜的的內容。
3.1 transient關鍵字的使用
正如上面所述,Java序列化的的是物件的非靜態欄位及其值。而transient關鍵字正是使用在實現了Serializable介面的目標類的欄位中,凡是被該關鍵字修飾的欄位,都將被序列化過濾掉,即不會被序列化。
將上面的例子中Student類中的phone欄位前面加上transient關鍵字:
public class Student implements Serializable { //... private transient String phone; //... }
執行結果變為:
姓名:張三 年齡:32 性別:man 地址:北京 手機:null
可見由於phone欄位添加了transient關鍵字,在序列化的時候,其值未進行序列化,反序列化回來之後其值將會是null。
3.2 writeObject方法的使用
writeObject()是在ObjectOutputStream中定義的方法,使用這個方法可以將目標物件寫入到流中,從而實現物件序列化。但是Java為我們提供了自定義writeObject()方法的功能,當我們在目標類中自定義writeObject()方法之後,將會首先呼叫我們自定義的方法,然後在繼續執行原有的方法步驟(使用defaultWriteObject方法)。這樣的功能為我們在物件序列化之前可以對物件的欄位進行有一些附加操作,最為常用的就是針對一些需要保密的欄位(比如密碼欄位),進行有效的加密措施,保證持久化資料的安全性。
這裡我對Student類新增password欄位,和對應的set和get方法。
public class Student implements Serializable { //... private String password; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
然後在Student類中定義writeObject()方法:
public class Student implements Serializable { //... private void writeObject(ObjectOutputStream oos) throws IOException{ password = Integer.valueOf(Integer.valueOf(password).intValue() << 2).toString(); oos.defaultWriteObject(); } }
這裡我對密碼欄位的值以左移兩位的方式進行簡單加密,然後呼叫ObjectOutputStream中的defaultWriteObject()方法來返回原來的序列化執行步驟。具體的呼叫棧如下:
writeObject->writeObject0->writeOrdinaryObject->writeSerialData->invokeWriteObject->invoke(呼叫自定義的writeObject)->defaultWriteObject->defaultWriteFields->writeObject0->...
在目標類中增加writeObject方法之後,我們通過上面的呼叫棧可以看到,呼叫順序會在writeSerialData這裡發生轉折,執行invokeWriteObject方法,呼叫目標類中的writeObject方法,然後再經過defaultWriteObject方法重回原來的步驟,這表明自定義的writeObject方法操作會優先執行。
這樣設定之後,序列化完成後,儲存到檔案中的將會是加密後的密碼值,我們結合下一個內容readObject方法進行測試。
3.3 readObject方法的使用
該方法是與writeObject方法相對應的,是用於讀取序列化內容的方法,用於反序列化過程中。類似於writeObject方法的自定義,我們進行readObject方法的自定義:
public class Student implements Serializable { //... private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException{ ois.defaultReadObject(); if(password != null) password = Integer.valueOf(Integer.valueOf(password).intValue() >> 2).toString(); } }
在測試程式中新增密碼欄位:
public class SerilizeTest { public static void main(String[] args) { serilize(); Student s = (Student) deserilize(); System.out.println("姓名:" + s.getName()+"\n年齡:"+ s.getAge()+"\n性別:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手機:"+s.getPhone()+"\n密碼:"+s.getPassword()); } //... public static void serilize() { Student s = new Student(); s.setName("張三"); s.setAge(32); s.setSex("man"); s.setAddress("北京"); s.setPhone("12345678910"); s.setPassword("123456"); OutputStream os = null; ObjectOutputStream oos = null; File f = new File("D:/student.out"); //... } }
執行程式結果為:
姓名:張三 年齡:32 性別:man 地址:北京 手機:null 密碼:123456
這裡的密碼經過了序列化時的加密與反序列化時的加密操作,由於前後結果一致,無法看出變化,簡單的做法就是將解密演算法改變:
public class Student implements Serializable { //... private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException{ ois.defaultReadObject(); if(password != null) password = Integer.valueOf(Integer.valueOf(password).intValue() >> 3).toString(); } }
這裡將解密的演算法改為將目標值右移三位,這樣就會導致最後獲取到的密碼值與原設定的“123456”不同。執行結果如下:
姓名:張三 年齡:32 性別:man 地址:北京 手機:null 密碼:61728
3.4 writeReplace方法的使用
Java的序列化並不是dead的,而是非常的靈活,我們甚至可以在序列化的時候改變目標的型別,這就需要writeReplace方法來操作。
我們在目標類中自定義writeReplace方法,該方法用於返回一個Object型別,這個Object就是你改變之後的型別,序列化的過程中會判斷目標類中是否存在writeObject方法,若存在該方法,就會實行呼叫,採用該方法返回的型別物件作為序列化的新目標物件。
現在我們在Student類中自定義writeReplace方法:
public class Student implements Serializable { //... private Object writeReplace() throws ObjectStreamException{ StringBuffer sb = new StringBuffer(); String s = sb.append(name).append(",").append(age).append(",").append(sex).append(",").append(address).append(",").append(phone).append(",").append(password).toString(); return s; } }
通過自定義的writeReplace方法將目標類中的資料整合轉化為一個字串,並將這個字串作為新目標物件進行序列化。
執行之後會報錯:
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to xuliehua.Student at xuliehua.SerilizeTest.deSerilize(SerilizeTest.java:32) at xuliehua.SerilizeTest.main(SerilizeTest.java:20)
提示在反序列化時,字串型別不能強轉為Student型別,這說明,我們儲存到檔案中的序列化內容為字串型別,也就是說我們自定義的writeReplace方法起作用了。
現在我們來對反序列化方法進行些許修改,來準確的獲取序列化的內容。
public class SerilizeTest { public static void main(String[] args) { serilize(); Student s = (Student) deserilize(); //System.out.println("姓名:" + s.getName()+"\n年齡:"+ s.getAge()+"\n性別:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手機:"+s.getPhone()+"\n密碼:"+s.getPassword()); System.out.println(s); } //... public static String deSerilize(){ //Student s = new Student(); String s = ""; InputStream is = null; ObjectInputStream ois = null; File f = new File("D:/student.out"); try { is = new FileInputStream(f); ois = new ObjectInputStream(is); //s = (Student)ois.readObject(); s = (String)ois.readObject(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); }finally{ if(ois != null){ try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } if(is != null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return s; } }
再來執行一下:
張三,32,man,北京,12345678910,123456
準確獲取序列化內容。
這裡需要注意一點,當我們使用這種方式來改變目標物件型別後,原本型別中標識為transient的欄位的過濾功能將會失效,因為我們序列化的目標發生的轉移,自然原型別欄位上設定的transient不會對新型別起任何作用,就比如此處的phone欄位。
3.5 readResolve方法的使用
與writeReplace方法對應的,我們也可以在反序列化的時候對目標的型別進行更改,這需要使用readResolve方法,使用方式是在目標類中自定義readResolve方法,該方法的返回值為Object物件,即轉換的新型別物件。
這裡我們在3.3 的基礎上進行程式碼修改,首先我們在Student類中自定義readResolve方法:
public class Student implements Serializable { //... //private Object writeReplace() throws ObjectStreamException{ //StringBuffer sb = new StringBuffer(); //String s = sb.append(name).append(",").append(age).append(",").append(sex).append(",").append(address).append(",").append(phone).append(",").append(password).toString(); //return s; //} private Object readResolve()throws ObjectStreamException{ Map<String,Object> map = new HashMap<String,Object>(); map.put("name", name); map.put("age", age); map.put("sex", sex); map.put("address", address); map.put("phone", phone); map.put("password", password); return map; } }
在這個方法中我們將獲取的資料儲存到一個Map集合中,並將這個集合返回。
直接執行程式會報錯:
Exception in thread "main" java.lang.ClassCastException: java.util.HashMap cannot be cast to xuliehua.Student at xuliehua.SerilizeTest.deSerilize(SerilizeTest.java:32) at xuliehua.SerilizeTest.main(SerilizeTest.java:20)
報錯說明我們設定的readResolve方法被執行了,因為型別無法進行轉化,所以報錯,我們作如下修改:
public class SerilizeTest { public static void main(String[] args) { serilize(); //Student s = (Student) deserilize(); //System.out.println("姓名:" + s.getName()+"\n年齡:"+ s.getAge()+"\n性別:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手機:"+s.getPhone()+"\n密碼:"+s.getPassword()); Map<String,Object> map = deSerilize(); System.out.println(map); } //... @SuppressWarnings("unchecked") public static Map<String,Object> deSerilize(){ Map<String,Object> map = new HashMap<String,Object>(); //Student s = new Student(); InputStream is = null; ObjectInputStream ois = null; File f = new File("D:/student.out"); try { is = new FileInputStream(f); ois = new ObjectInputStream(is); //s = (Student)ois.readObject(); map = (Map<String,Object>)ois.readObject(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); }finally{ if(ois != null){ try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } if(is != null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return map; } }
執行結果:
{phone=null, sex=man, address=北京, age=32, name=張三, password=61728}
可見我們可以準確獲取到資料,而且是以改變後的型別。
注意:writeObject方法與readObject方法可以同時存在,但是一般情況下writeReplace方法與readResolve方法是不同時使用的。因為二者均是基於原型別來進行轉換,如果同時存在,那麼兩個新型別之間是無法進行型別轉換的(當然如果這兩個型別是存在繼承關係的除外),功能無法實現。