黑馬程序員——————> 自定義序列化


在一些特殊的場景下,如果一個類里包含的某些實例變量是敏感信息,例如銀行賬戶信息,這時不希望系統將該實例變量值進行實例化;或者某個實例變量的類型是不可序列化的,因此不希望對該實例變量進行遞歸實例化,以避免引發異常。

------- android培訓java培訓、期待與您交流! ----------

 

通過在實例變量前面使用transient關鍵字修飾,可以指定java序列化時無須理會該實例變量。如下Person類與前面的Person類幾乎完全一樣,只是它的age使用了transient關鍵字修飾。

 1 public class Person implements Serializable 
 2 {
 3     private String name;
 4     //transient只能修飾實例變量,不可修飾java程序中的其他成分
 5     private transient int age;
 6     
 7     //此處沒有提供無參構造
 8     public Person(String name, int age)
 9     {
10         System.out.println("有參數的構造器");
11         this.name = name;
12         this.age = age;
13     }
14 
15     public String getName() {
16         return name;
17     }
18 
19     public void setName(String name) {
20         this.name = name;
21     }
22 
23     public int getAge() {
24         return age;
25     }
26 
27     public void setAge(int age) {
28         this.age = age;
29     }
30     
31 }

下面程序先序列化一個Person對象,然后再反序列化該Person對象,得到反序列化的Person對象后程序輸出該對象的age實例變量值。

 1 public class TransientTest 
 2 {
 3     public static void main(String[] args) 
 4     {
 5         try(
 6                 //創建一個ObjectOutputStream輸出流
 7                 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("transient.txt"));
 8                 //創建一個ObjectInputStream輸入流
 9                 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("transient.txt")))
10         {
11             Person per = new Person("孫悟空",500);
12             //系統將per對象轉換成了字節序列輸出
13             oos.writeObject(per);
14             Person p = (Person) ois.readObject();
15             System.out.println(p.getAge()+""+p.getName());
16         }
17         catch(Exception ex)
18         {
19             ex.printStackTrace();
20         }
21 
22     }
23 
24 }

上面程序分別為Person對象的兩個實例變量指定了值。由於本程序中的Preson類的age實例變量使用transient關鍵字修飾,所以程序代碼將輸出0;

 

使用transient關鍵字修飾實例變量雖然簡單方便,但被transient修飾的實例變量將被完全隔離在序列化機制之外,這樣導致在反序列化回復java對象時無法取得該實例變量的值。java還提供了一種自定義序列化機制,通過這種自定義序列化機制可以讓程序控制如何序列化各實例變量,甚至完全不序列化某些實例變量(與使用transient關鍵字的效果相同)。

在序列化和反序列化過程中需要特殊處理的類應該提供如下特殊簽名的方法,這些特殊的方法用以實現自定義序列化。

private void writeObject(java.io.ObjectOutputStream out)throws IOException

private void readObject(java.io.ObjectInputStream in)throws IOException,ClassNotFoundException;

private void readObjectNoData() throws ObjectStreamException;

writeObject()方法負責寫入特定類的實例狀態,以便相應的readObject()方法可以恢復它。通過重寫該方法,程序員可以完全獲得對序列化機制的控制,可以自主決定哪些實例變量需要序列化,需要怎樣序列化。在默認情況下,該方法會調用out.defaultWriteObject來保存java 對象的各實例變量,從而可以實現序列化java對象狀態的目的。

 

readObject()方法負責從流中讀取並恢復對象的實例變量,通過重寫該方法,程序員可以完全獲得對反序列化機制的控制,可以自主決定需要反序列化哪些實例變量,以及如何進行反序列化。在默認情況下,該方法會調用in.defaultReadObject來恢復java對象的非瞬態實例變量。在通常情況下,readObject()方法與writeObject()方法對應,如果writeObject()方法中對java對象的實例變量進行了一些處理,則應該在readObject()方法中對其實例變量進行相應的反處理,以便正確恢復該對象。

 

當序列化流不完整時,readObjectNoData()方法可以用來正確的初始化反序列化的對象。例如,接收方使用的反序列化類的版本不同於發送方,或者接收方版本擴展的類不是發送方版本擴展的類,或者序列化流被篡改時,系統都會調用readObjectNoData()方法來初始化反序列化對象。

 

下面的Person類提供了writeObject()和readObject()兩個方法,其中writeObject()方法在保存Person對象時將其name實例變量包裝成StringBuffer,並將其字符序列反轉后寫入;在readObject()方法中處理name的策略與此對應,先將讀取的數據強制類型轉換成StringBuffer,再將其反轉后賦給name實例。

 1 public class Person implements Serializable
 2 {
 3     private String name;
 4     private int age;
 5     //此處沒有提供無參構造
 6     public Person(String name,int age)
 7     {
 8         System.out.println("有參數的構造器");
 9         this.name = name;
10         this.age = age;
11     }
12     public String getName() {
13         return name;
14     }
15     public void setName(String name) {
16         this.name = name;
17     }
18     public int getAge() {
19         return age;
20     }
21     public void setAge(int age) {
22         this.age = age;
23     }
24     
25     
26     
27     private void writeObject(java.io.ObjectOutputStream out) throws IOException
28     {
29         //將name實例變量值反轉后寫入二進制流
30         out.writeObject(new StringBuffer(name).reverse());
31         out.writeInt(age);
32     }
33     
34     private void readObject(java.io.ObjectInputStream in) throws Exception
35     {
36         //將讀取的字符串反轉后賦給name變量
37         this.name = ((StringBuffer)in.readObject()).reverse().toString();
38         this.age = in.readInt();
39     }
40 }

上面程序中的方法用以實現自定義序列化,對於這個Preson類而言,序列化,反序列化Preson實例並沒有什么區別,去別在於序列化后的對象流,即使有Cracker截獲到Person對象流,他看到的name也是加密后的name值,這樣就提高了序列化的安全性。

 

 

 

還有一種更徹底的自定義機制,它甚至可以在序列化對象時將該對象替換成其他對象。如果需要實現序列化某個對象時替換該對象,則應為序列化類提供如下特殊方法。

ANY-ACCESS-MODIFIER Object writeReplace()

 

此writeReplace()方法將由序列化機制調用,只要該方法存在。因為該方法可以擁有私有(private),受保護的(protected),和包私有(package-private)等訪問權限,所以其子類有可能獲得該方法。例如下面的Person類提供了writeReplace()方法,這樣可以在寫入Person對象時將該對象替換成ArrayList.

 1 public class Person implements Serializable 
 2 {
 3     private String name;
 4     private int age;
 5     //注意此處沒有提供無參構造
 6     public Person(String name, int age)
 7     {
 8         System.out.println("帶參構造器");
 9         this.name = name;
10         this.age = age;
11     }
12     public String getName() {
13         return name;
14     }
15     public void setName(String name) {
16         this.name = name;
17     }
18     public int getAge() {
19         return age;
20     }
21     public void setAge(int age) {
22         this.age = age;
23     }
24     
25     //重寫writeReplace方法,程序在序列化該對象之前,先調用該方法
26     private Object writeReplace()
27     {
28         ArrayList<Object> list = new ArrayList<Object>();
29         list.add(name);
30         list.add(age);
31         return list;
32         
33     }
34     
35 }

 

java的序列化機制保證在序列化某個對象之前,先調用該對象的writeReplace()方法,如果該方法返回另一個java對象,則系統轉為序列化另一個對象。如下程序表面上是序列化Preson對象,但實際上序列化的是ArrayList.

 1 public class ReplaceTest 
 2 {
 3     public static void main(String[] args) 
 4     {
 5         try(
 6             //創建一個ObjectOutputStream輸出流
 7             ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("replace.txt"));
 8             //創建一個ObjectInputStream輸入流
 9             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("replace.txt")))
10         {
11             Person per = new Person("段亞東",25);
12             //系統將per對象轉換成字節序列輸出
13             oos.writeObject(per);
14             //反序列化讀取到的是ArrayList
15             ArrayList list = (ArrayList)ois.readObject();
16             System.out.println(list);
17         }
18         catch(Exception ex)
19         {
20             ex.printStackTrace();
21         }
22 
23     }
24 
25 }

 

 上面程序使用writeObject()寫入了一個Person對象,但第二行代碼使用readObject()方法返回的實際上是一個ArrayList對象,這是因為Person類的writeReplace()方法返回了一個ArrayList對象,所以序列化機制在序列化Person對象時,實際上是轉為序列化ArrayList對象。

 

根據上面的介紹,可以知道系統在序列化某個對象之前,會先調用該對象的writeReplace()和writeObject()兩個方法,系統總是先調用被序列化對象的writeReplace()方法,如果該方法返回另一個對象,系統將再次調用另一個對象的writeReplace()方法,直到該方法不再返回另一個對象為止,程序最后將調用該對象的writeObject()方法來保存該對象的狀態。

 

 

與writeReplace()方法相對應的是,序列化機制里還有一個特殊的方法,它可以實現保護性復制整個對象,這個方法就是

Object  readResolve()

 這個方法會緊挨着readObject()之后被調用,該方法的返回值將會代替原來反序列化的對象,而原來readObject()反序列化的對象將會立即丟棄。

readObject()方法在序列化單例類,枚舉類時尤其有用。當然,如果使用java5提供的enum來定義枚舉類,則完全不用擔心,程序沒有任何問題,但如果應用中有早期遺留下來的枚舉類,例如下面的Orientation類就是一個枚舉類。

public class Orientation
{
  public static final Orientation HORIZONTAL = new Orientation(1);

  public static final Orientation VERTICAL = new Orientation(2);

  private int value;

  private Orientation(int value)
  {
    this.value = value;
  }
}

在java5以前,這種代碼是很常見的。Orientation類的構造私有,程序只有兩個Orientation對象,分別通過Orientation的兩個常量來引用。但如果讓該類實現Serializable接口,則會引發一個問題,如果將一個Orientation.HORIZONTALZ值序列化后再讀出,如下代碼所示:

 1 public class OrientationDemo
 2 {
 3     public static void main(String[] args) throws Exception 
 4     {
 5         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("transient.txt"));
 6         
 7         //寫入Orientation.HORIZONTAL值
 8         oos.writeObject(Orientation.HORIZONTAL);
 9         
10         //創建一個ObjectInputStream輸入流
11         ObjectInputStream ois = new ObjectInputStream(new FileInputStream("transient.txt"));
12         
13         //讀取剛剛序列化的值
14         Orientation ori = (Orientation) ois.readObject();
15         
16         System.out.println(ori == Orientation.HORIZONTAL);//false
17     }
18     
19 }

兩個值比較,返回falase,也就是說,ori是一個新的Orientation對象,而不等於Orientation類中的任何枚舉值,雖然Orientation的構造器是私有的,但反序列化依然可以創建Orientation對象。

反序列化機制在恢復java對象時無須調用構造器來初始化java對象。從這個意義上來看,序列化機制可以用來"克隆"對象。

 

在這種情況下,可以通過為Orientation類提供一個readResolve()方法來解決該問題,readResolve()方法的返回值將會代替原來反序列化的對象,也就是讓反序列化得到的Orientation對象被直接丟棄。下面是為Orientation類提供的readResolve()方法。

     private Object readResolve() throws ObjectStreamException
     {
        if(value == 1)
        {
            return HORIZONTAL;
        }
        if(value == 2)
        {
            return VERTICAL;
        }
         return null;     
     }

通過重寫readResolve()方法可以保證反序列化得到的依然是Orientation的HORIZOHTAL或VERTICAL兩個枚舉值之一。

 

所有單例類,枚舉類在實現序列化時都應該提供readResolve()方法,這樣才可以保證反序列化的對象依然正常。

 

與readReplace()方法類似的是,readResolve()方法也可以使用任意的訪問控制符,因此父類的readResolve()方法可能被其子類繼承。這樣利用readResolve()方法時就會存在一個明顯的缺點,就是當父類已經實現了readResolve()方法后,子類將變得無從下手。如果父類包含一個protected或public的readResolve()方法,而且子類也沒有重寫該方法,將會使得子類反序列化時得到一個父類的對象,這顯然不是程序要的結果,而且也不容易發現這種錯誤。總是讓子類重寫readResolve()方法無疑是一個負擔,因此對於要被作為父類繼承的類而言,實現readResolve()方法可能有一些潛在的危險。

通常的建議是,對於final類重寫readResolve()方法不會有任何問題:否則,重寫readResolve()方法時應盡量使用private修飾該方法。

 

 

另一種自定義序列化機制

java還提供了另一種序列化機制,這種序列化方式完全由程序員決定存儲和恢復對象數據。要實現該目標,java類必須實現Externalizable接口,該接口里定義了如下兩個方法。

void readExternal(ObjectInput in): 需要序列化的類實現 該方法來實現反序列化。該方法調用DataInput(它是ObjectInput 的父接口) 的方法來恢復基本類型的實例變量值,調用ObjectInput的readObject()方法來恢復引用類型的實例變量值。

void writeExternal(Object out): 需要序列化的類實現該方法來保存對象的狀態。該方法調用DataOutput(它是ObjectOutput 的父接口)的方法來保存基本類型的實例變量值,調用ObjectOutput的writeObject()方法來保存引用類型的實例變量值。

 

實際上,采用實現Externalizable接口方式的序列化與前面介紹的自定義序列化非常相似,只是Externalizable接口強制自定義序列化。下面的Person類實現了Externalizable接口,並且實現了該接口里提供的兩個方法,用以實現自定義序列化。

 1 public class Person implements Externalizable
 2 {    
 3     private String name;
 4     private int age;
 5     //此處必須提供無參構造,否則反序列化時會失效
 6     public Person(){}
 7     public Person(String name, int age)
 8     {
 9         System.out.println("有參數的構造器");
10         this.name = name;
11         this.age = age;
12     }
13     
14 
15     public String getName() {
16         return name;
17     }
18     public void setName(String name) {
19         this.name = name;
20     }
21     public int getAge() {
22         return age;
23     }
24     public void setAge(int age) {
25         this.age = age;
26     }
27     
28     @Override
29     public void writeExternal(java.io.ObjectOutput out) throws IOException 
30     {
31         //將name實例變量值反轉后寫入二進制流
32         out.writeObject(new StringBuffer(name).reverse());
33         out.writeInt(age);
34 
35     }
36 
37     @Override
38     public void readExternal(java.io.ObjectInput in) throws IOException, ClassNotFoundException 
39     {
40         ------- android培訓java培訓、期待與您交流! ----------
#008000;">//將讀取的字符串反轉后賦給name實例變量
41         this.name = ((StringBuffer)in.readObject()).reverse().toString();
42         this.age = in.readInt();
43 
44     }
45 
46 }

上面程序中Person類實現了Externalizable接口,該Person類還實現了readExternal(),writeExternal()兩個方法,這兩個方法除了方法簽名和readObject(),writeObject()兩個方法的方法簽名不同之外,其方法體完全一樣。

如果程序需要序列化實現Externalizable接口的對象,一樣調用ObjectOutputStream的writeObject()方法輸出該對象即可;反序列化該對象,則調用ObjectInputStream的readObject()方法。

 

需要指出的是,當使用Externalizable機制反序列化該對象時,程序會使用public的無參構造器創建實例,然后才執行readExternal()方法進行反序列化,因此實現Externalizable的序列化類必須提供public的無參構造。

 

雖然實現Externalizable接口能帶來一定的性能提升,但由於實現ExternaLizable接口導致了編程復雜度的增加,所以大部分時候都是采用實現Serializable接口方式來實現序列化。


免責聲明!

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



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