Java基礎系列——序列化(一)


原創作品,可以轉載,但是請標注出處地址:http://www.cnblogs.com/V1haoge/p/6797659.html

  工作中發現,自己對Java的了解還很片面,沒有深入的研究,有很多的JDK API都知之甚少,遂決定強化JDK的學習,並記錄下自己的學習經驗,供自己查閱。

  首先研究的就是Java中的序列化機制。

1、序列化簡介

  在項目中有很多情況需要對實例對象進行序列化與反序列化,這樣可以持久的保存對象的狀態,甚至在各個組件之間進行對象傳遞和遠程調用。序列化機制是項目中必不可少的常用機制。

  要想一個類擁有序列化、反序列化功能,最簡單的方法就是實現java.io.Serializable接口,這個接口是一個標記接口(marker Interface),即其內部無任何字段與方法定義。

  當我們定義了一個實現Serializable接口的類之后,一般我們會手動在類內部定義一個private static final long serialVersionUID字段,用來保存當前類的序列版本號。這樣做的目的就是唯一標識該類,其對象持久化之后這個字段將會保存到持久化文件中,當我們對這個類做了一些更改時,新的更改可以根據這個版本號找到已持久化的內容,來保證來自類的更改能夠准確的體現到持久化內容中。而不至於因為未定義版本號,而找不到原持久化內容。

  當然如果我們不實現Serializable接口就對該類進行序列化與反序列化操作,那么將會拋出java.io.NotSerializableException異常。

  如下例子:

 1 package xuliehua;  2  3 import java.io.Serializable;  4 public class Student implements Serializable {  5  6 private static final long serialVersionUID = -3111843137944176097L;  7  8 private String name;  9 private int age; 10 private String sex; 11 private String address; 12 private String phone; 13 public String getName() { 14 return name; 15  } 16 public void setName(String name) { 17 this.name = name; 18  } 19 public int getAge() { 20 return age; 21  } 22 public void setAge(int age) { 23 this.age = age; 24  } 25 public String getSex() { 26 return sex; 27  } 28 public void setSex(String sex) { 29 this.sex = sex; 30  } 31 public String getAddress() { 32 return address; 33  } 34 public void setAddress(String address) { 35 this.address = address; 36  } 37 public String getPhone() { 38 return phone; 39  } 40 public void setPhone(String phone) { 41 this.phone = phone; 42  } 43 }

2、序列化的使用

  雖然要實現序列化只需要實現Serializable接口即可,但這只是讓類的對象擁有可被序列化和反序列化的功能,它自己並不會自動實現序列化與反序列化,我們需要編寫代碼來進行序列化與反序列化。

  這就需要使用ObjectOutputStream類的writeObject()方法與readObject()方法,這兩個方法分別對應於將對象寫入到流中(序列化),從流中讀取對象(反序列化)。

  Java中的對象序列化,序列化的是什么?答案是對象的狀態、更具體的說就是對象中的字段及其值,因為這些值正好描述了對象的狀態。

  下面的例子我們實現將Student類的一個實例持久化到本地文件“D:/student.out”中,並從本地文件中讀到內存,這要借助於FileOutputStream和FileInputStream來實現:

 1 package xuliehua;  2  3 import java.io.File;  4 import java.io.FileInputStream;  5 import java.io.FileNotFoundException;  6 import java.io.FileOutputStream;  7 import java.io.IOException;  8 import java.io.InputStream;  9 import java.io.ObjectInputStream; 10 import java.io.ObjectOutputStream; 11 import java.io.OutputStream; 12 13 public class SerilizeTest { 14 15 public static void main(String[] args) { 16  serilize(); 17 Student s = (Student) deserilize(); 18 System.out.println("姓名:" + s.getName()+"\n年齡:"+ s.getAge()+"\n性別:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手機:"+s.getPhone()); 19  } 20 21 public static Object deserilize(){ 22 Student s = new Student(); 23 InputStream is = null; 24 ObjectInputStream ois = null; 25 File f = new File("D:/student.out"); 26 try { 27 is = new FileInputStream(f); 28 ois = new ObjectInputStream(is); 29 s = (Student)ois.readObject(); 30 } catch (FileNotFoundException e) { 31  e.printStackTrace(); 32 } catch (IOException e) { 33  e.printStackTrace(); 34 } catch (ClassNotFoundException e) { 35  e.printStackTrace(); 36 }finally{ 37 if(ois != null){ 38 try { 39  ois.close(); 40 } catch (IOException e) { 41  e.printStackTrace(); 42  } 43  } 44 if(is != null){ 45 try { 46  is.close(); 47 } catch (IOException e) { 48  e.printStackTrace(); 49  } 50  } 51  } 52 return s; 53  } 54 55 public static void serilize() { 56 Student s = new Student(); 57 s.setName("張三"); 58 s.setAge(32); 59 s.setSex("man"); 60 s.setAddress("北京"); 61 s.setPhone("12345678910"); 62 // s.setPassword("123456"); 63 OutputStream os = null; 64 ObjectOutputStream oos = null; 65 File f = new File("D:/student.out"); 66 try { 67 os = new FileOutputStream(f); 68 oos = new ObjectOutputStream(os); 69  oos.writeObject(s); 70 } catch (FileNotFoundException e) { 71  e.printStackTrace(); 72 } catch (IOException e) { 73  e.printStackTrace(); 74 }finally{ 75 if(oos != null) 76 try { 77 oos.close(); 78 } catch (IOException e) { 79 e.printStackTrace(); 80 } 81 if(os != null) 82 try { 83 os.close(); 84 } catch (IOException e) { 85 e.printStackTrace(); 86 } 87 } 88 } 89 }

  通過以上的代碼就可以實現簡單的對象序列化與反序列化。

執行結果:

姓名:張三
年齡:32
性別:man
地址:北京
手機:12345678910

  這里將writeObject的調用棧羅列出來:

writeObject->writeObject0->writeOrdinaryObject->writeSerialData->defaultWriteFields->writeObject0->...

  調用棧最后返回了writeObject0方法,這是使用遞歸的方式來遍歷目標類的字段中所有普通實現Serializable接口的類型字段,將其全部寫入流中,最后所有的寫入都會在writeObject0方法中終結,這個方法會根據字段的類型來調用響應的write方法進行流寫入。

  Java序列化的是對象的字段,但是這些字段並不一定都是簡單的String、或者是Integer之類,可能也是很復雜的類型,一個實現了Serializable接口的類類型,這時候我們序列化的時候,就需要將這個內部的第二層次的對象進行遞歸序列化,這種嵌套可以有無數層,但是總會有個終結。

3、自定義序列化功能

  上面的內容都是簡單又簡單,真正要注意的內容在這里,有關自定義序列化策略的內容才是序列化機制中最重要、最復雜的的內容。

3.1 transient關鍵字的使用

  正如上面所述,Java序列化的的是對象的非靜態字段及其值。而transient關鍵字正是使用在實現了Serializable接口的目標類的字段中,凡是被該關鍵字修飾的字段,都將被序列化過濾掉,即不會被序列化。

  將上面的例子中Student類中的phone字段前面加上transient關鍵字:

1     private transient String phone;

執行結果變為:

姓名:張三
年齡:32
性別:man
地址:北京
手機:null

  可見由於phone字段添加了transient關鍵字,在序列化的時候,其值未進行序列化,反序列化回來之后其值將會是null。

3.2 writeObject方法的使用

  writeObject()是在ObjectOutputStream中定義的方法,使用這個方法可以將目標對象寫入到流中,從而實現對象序列化。但是Java為我們提供了自定義writeObject()方法的功能,當我們在目標類中自定義writeObject()方法之后,將會首先調用我們自定義的方法,然后在繼續執行原有的方法步驟(使用defaultWriteObject方法)。這樣的功能為我們在對象序列化之前可以對對象的字段進行有一些附加操作,最為常用的就是針對一些需要保密的字段(比如密碼字段),進行有效的加密措施,保證持久化數據的安全性。

  這里我對Student類添加password字段,和對應的set和get方法。

1     private String password; 2 public String getPassword() { 3 return password; 4  } 5 public void setPassword(String password) { 6 this.password = password; 7 }

  然后在Student類中定義writeObject()方法:

1     private void writeObject(ObjectOutputStream oos) throws IOException{ 2 password = Integer.valueOf(Integer.valueOf(password).intValue() << 2).toString(); 3  oos.defaultWriteObject(); 4 }

  這里我對密碼字段的值以左移兩位的方式進行簡單加密,然后調用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方法的自定義:

1     private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException{ 2  ois.defaultReadObject(); 3 if(password != null) 4 password = Integer.valueOf(Integer.valueOf(password).intValue() >> 2).toString(); 5 }

  在測試程序中添加密碼字段:

18        System.out.println("姓名:" + s.getName()+"\n年齡:"+ s.getAge()+"\n性別:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手機:"+s.getPhone()+"\n密碼:"+s.getPassword());
62        s.setPassword("123456");

  執行程序結果為:

姓名:張三
年齡:32 性別:man 地址:北京 手機:null 密碼:123456

  這里的密碼經過了序列化時的加密與反序列化時的加密操作,由於前后結果一致,無法看出變化,簡單的做法就是將解密算法改變:

1     private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException{ 2  ois.defaultReadObject(); 3 if(password != null) 4 password = Integer.valueOf(Integer.valueOf(password).intValue() >> 3).toString(); 5 }

  這里將解密的算法改為將目標值右移三位,這樣就會導致最后獲取到的密碼值與原設置的“123456”不同。執行結果如下:

姓名:張三
年齡:32 性別:man 地址:北京 手機:null 密碼:61728

3.4 writeReplace方法的使用

  Java的序列化並不是dead的,而是非常的靈活,我們甚至可以在序列化的時候改變目標的類型,這就需要writeReplace方法來操作。

  我們在目標類中自定義writeReplace方法,該方法用於返回一個Object類型,這個Object就是你改變之后的類型,序列化的過程中會判斷目標類中是否存在writeObject方法,若存在該方法,就會實行調用,采用該方法返回的類型對象作為序列化的新目標對象。

  現在我們在Student類中自定義writeReplace方法:

1     private Object writeReplace() throws ObjectStreamException{
2         StringBuffer sb = new StringBuffer();
3         String s = sb.append(name).append(",").append(age).append(",").append(sex).append(",").append(address).append(",").append(phone).append(",").append(password).toString();
4         return s;
5     }

  通過自定義的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方法起作用了。

  現在我們來對反序列化方法進行些許修改,來准確的獲取序列化的內容。

 1     public static void main(String[] args) {
 2         serilize();
 3         String s = deSerilize();
 4 //        System.out.println("姓名:" + s.getName()+"\n年齡:"+ s.getAge()+"\n性別:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手機:"+s.getPhone());
 5         System.out.println(s);
 6     }
 7     
 8     public static String deSerilize(){
 9 //        Student s = new Student();
10         String s = "";
11         InputStream is = null;
12         ObjectInputStream ois = null;
13         File f = new File("D:/student.out");
14         try {
15             is = new FileInputStream(f);
16             ois = new ObjectInputStream(is);
17 //            s = (Student)ois.readObject();
18             s = (String)ois.readObject();
19         } catch (FileNotFoundException e) {
20             e.printStackTrace();
21         } catch (IOException e) {
22             e.printStackTrace();
23         } catch (ClassNotFoundException e) {
24             e.printStackTrace();
25         }finally{
26             if(ois != null){
27                 try {
28                     ois.close();
29                 } catch (IOException e) {
30                     e.printStackTrace();
31                 }
32             }
33             if(is != null){
34                 try {
35                     is.close();
36                 } catch (IOException e) {
37                     e.printStackTrace();
38                 }
39             }
40         }
41         return s;
42     }

  再來執行一下:

張三,32,man,北京,12345678910,123456

  准確獲取序列化內容。

  這里需要注意一點,當我們使用這種方式來改變目標對象類型后,原本類型中標識為transient的字段的過濾功能將會失效,因為我們序列化的目標發生的轉移,自然原類型字段上設置的transient不會對新類型起任何作用,就比如此處的phone字段。

3.5 readResolve方法的使用

  與writeObject方法對應的,我們也可以在反序列化的時候對目標的類型進行更改,這需要使用readResolve方法,使用方式是在目標類中自定義readResolve方法,該方法的返回值為Object對象,即轉換的新類型對象。

  這里我們在3.3 的基礎上進行代碼修改,首先我們在Student類中自定義readResolve方法:

 1     private Object readResolve()throws ObjectStreamException{
 2         Map<String,Object> map = new HashMap<String,Object>();        
 3         map.put("name", name);
 4         map.put("age", age);
 5         map.put("sex", sex);
 6         map.put("address", address);
 7         map.put("phone", phone);
 8         map.put("password", password);
 9         return map;
10     }

  在這個方法中我們將獲取的數據保存到一個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方法被執行了,因為類型無法進行轉化,所以報錯,我們作如下修改:

 1     public static void main(String[] args) {
 2         serilize();
 3 //        Student s = deSerilize();
 4 //        System.out.println("姓名:" + s.getName()+"\n年齡:"+ s.getAge()+"\n性別:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手機:"+s.getPhone()+"\n密碼:"+s.getPassword());
 5         Map<String,Object> map = deSerilize();
 6         System.out.println(map);
 7     }
 8     
 9     @SuppressWarnings("unchecked")
10     public static Map<String,Object> deSerilize(){
11         Map<String,Object> map = new HashMap<String,Object>();
12 //        Student s = new Student();
13         InputStream is = null;
14         ObjectInputStream ois = null;
15         File f = new File("D:/student.out");
16         try {
17             is = new FileInputStream(f);
18             ois = new ObjectInputStream(is);
19 //            s = (Student)ois.readObject();
20             map = (Map<String,Object>)ois.readObject();
21         } catch (FileNotFoundException e) {
22             e.printStackTrace();
23         } catch (IOException e) {
24             e.printStackTrace();
25         } catch (ClassNotFoundException e) {
26             e.printStackTrace();
27         }finally{
28             if(ois != null){
29                 try {
30                     ois.close();
31                 } catch (IOException e) {
32                     e.printStackTrace();
33                 }
34             }
35             if(is != null){
36                 try {
37                     is.close();
38                 } catch (IOException e) {
39                     e.printStackTrace();
40                 }
41             }
42         }
43         return map;
44     }

  執行結果:

{phone=null, sex=man, address=北京, age=32, name=張三, password=61728}

  可見我們可以准確獲取到數據,而且是以改變后的類型。

  注意:writeObject方法與readObject方法可以同時存在,但是一般情況下writeReplace方法與readResolve方法是不同時使用的。因為二者均是基於原類型來進行轉換,如果同時存在,那么兩個新類型之間是無法進行類型轉換的(當然如果這兩個類型是存在繼承關系的除外),功能無法實現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM