@OverWrite BlogJava

            BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理 ::
            128 隨筆 :: 0 文章 :: 29 評論 :: 0 Trackbacks

           
          默認的序列化機制并不難操縱。然而,假若有特殊要求又該怎么辦呢?我們可能有特殊的安全問題,不希望對象的某一部分序列化;或者某一個子對象完全不必序列化,因為對象恢復以后,那一部分需要重新創建。
          此時,通過實現Externalizable接口,用它代替Serializable接口,便可控制序列化的具體過程。這個Externalizable接口擴展了Serializable,并增添了兩個方法:writeExternal()和readExternal()。在序列化和重新裝配的過程中,會自動調用這兩個方法,以便我們執行一些特殊操作。
          下面這個例子展示了Externalizable接口方法的簡單應用。注意Blip1和Blip2幾乎完全一致,除了極微小的差別(自己研究一下代碼,看看是否能發現):

          //: Blips.java
          // Simple use of Externalizable & a pitfall
          import java.io.*;
          import java.util.*;

          class Blip1 implements Externalizable {
            
          public Blip1() {
              System.out.println(
          "Blip1 Constructor");
            }

            
          public void writeExternal(ObjectOutput out)
                
          throws IOException {
              System.out.println(
          "Blip1.writeExternal");
            }

            
          public void readExternal(ObjectInput in)
               
          throws IOException, ClassNotFoundException {
              System.out.println(
          "Blip1.readExternal");
            }

          }


          class Blip2 implements Externalizable {
            Blip2() 
          {
              System.out.println(
          "Blip2 Constructor");
            }

            
          public void writeExternal(ObjectOutput out)
                
          throws IOException {
              System.out.println(
          "Blip2.writeExternal");
            }

            
          public void readExternal(ObjectInput in)
               
          throws IOException, ClassNotFoundException {
              System.out.println(
          "Blip2.readExternal");
            }

          }


          public class Blips {
            
          public static void main(String[] args) {
              System.out.println(
          "Constructing objects:");
              Blip1 b1 
          = new Blip1();
              Blip2 b2 
          = new Blip2();
              
          try {
                ObjectOutputStream o 
          =
                  
          new ObjectOutputStream(
                    
          new FileOutputStream("Blips.out"));
                System.out.println(
          "Saving objects:");
                o.writeObject(b1);
                o.writeObject(b2);
                o.close();
                
          // Now get them back:
                ObjectInputStream in =
                  
          new ObjectInputStream(
                    
          new FileInputStream("Blips.out"));
                System.out.println(
          "Recovering b1:");
                b1 
          = (Blip1)in.readObject();
                
          // OOPS! Throws an exception:
          //!   System.out.println("Recovering b2:");
          //!   b2 = (Blip2)in.readObject();
              }
           catch(Exception e) {
                e.printStackTrace();
              }

            }

          }
           ///:~

           

          該程序輸出如下:

          Constructing objects:
          Blip1 Constructor
          Blip2 Constructor
          Saving objects:
          Blip1.writeExternal
          Blip2.writeExternal
          Recovering b1:
          Blip1 Constructor
          Blip1.readExternal
          未恢復Blip2對象的原因是那樣做會導致一個違例。你找出了Blip1和Blip2之間的區別嗎?Blip1的構建器是“公共的”(public),Blip2的構建器則不然,這樣便會在恢復時造成違例。試試將Blip2的構建器屬性變成“public”,然后刪除//!注釋標記,看看是否能得到正確的結果。
          恢復b1后,會調用Blip1默認構建器。這與恢復一個Serializable(可序列化)對象不同。在后者的情況下,對象完全以它保存下來的二進制位為基礎恢復,不存在構建器調用。而對一個Externalizable對象,所有普通的默認構建行為都會發生(包括在字段定義時的初始化),而且會調用readExternal()。必須注意這一事實——特別注意所有默認的構建行為都會進行——否則很難在自己的Externalizable對象中產生正確的行為。
          下面這個例子揭示了保存和恢復一個Externalizable對象必須做的全部事情:

           

          //: Blip3.java
          // Reconstructing an externalizable object
          import java.io.*;
          import java.util.*;

          class Blip3 implements Externalizable {
            
          int i;
            String s; 
          // No initialization
            public Blip3() {
              System.out.println(
          "Blip3 Constructor");
              
          // s, i not initialized
            }

            
          public Blip3(String x, int a) {
              System.out.println(
          "Blip3(String x, int a)");
              s 
          = x;
              i 
          = a;
              
          // s & i initialized only in non-default
              
          // constructor.
            }

            
          public String toString() return s + i; }
            
          public void writeExternal(ObjectOutput out)
                
          throws IOException {
              System.out.println(
          "Blip3.writeExternal");
              
          // You must do this:
              out.writeObject(s); out.writeInt(i);
            }

            
          public void readExternal(ObjectInput in)
               
          throws IOException, ClassNotFoundException {
              System.out.println(
          "Blip3.readExternal");
              
          // You must do this:
              s = (String)in.readObject(); 
              i 
          =in.readInt();
            }

            
          public static void main(String[] args) {
              System.out.println(
          "Constructing objects:");
              Blip3 b3 
          = new Blip3("A String "47);
              System.out.println(b3.toString());
              
          try {
                ObjectOutputStream o 
          =
                  
          new ObjectOutputStream(
                    
          new FileOutputStream("Blip3.out"));
                System.out.println(
          "Saving object:");
                o.writeObject(b3);
                o.close();
                
          // Now get it back:
                ObjectInputStream in =
                  
          new ObjectInputStream(
                    
          new FileInputStream("Blip3.out"));
                System.out.println(
          "Recovering b3:");
                b3 
          = (Blip3)in.readObject();
                System.out.println(b3.toString());
              }
           catch(Exception e) {
                e.printStackTrace();
              }

            }

          }
           ///:~



          其中,字段s和i只在第二個構建器中初始化,不關默認構建器的事。這意味著假如不在readExternal中初始化s和i,它們就會成為null(因為在對象創建的第一步中已將對象的存儲空間清除為1)。若注釋掉跟隨于“You must do this”后面的兩行代碼,并運行程序,就會發現當對象恢復以后,s是null,而i是零。
          若從一個Externalizable對象繼承,通常需要調用writeExternal()和readExternal()的基礎類版本,以便正確地保存和恢復基礎類組件。
          所以為了讓一切正常運作起來,千萬不可僅在writeExternal()方法執行期間寫入對象的重要數據(沒有默認的行為可用來為一個Externalizable對象寫入所有成員對象)的,而是必須在readExternal()方法中也恢復那些數據。初次操作時可能會有些不習慣,因為Externalizable對象的默認構建行為使其看起來似乎正在進行某種存儲與恢復操作。但實情并非如此。

          1. transient(臨時)關鍵字
          控制序列化過程時,可能有一個特定的子對象不愿讓Java的序列化機制自動保存與恢復。一般地,若那個子對象包含了不想序列化的敏感信息(如密碼),就會面臨這種情況。即使那種信息在對象中具有“private”(私有)屬性,但一旦經序列化處理,人們就可以通過讀取一個文件,或者攔截網絡傳輸得到它。
          為防止對象的敏感部分被序列化,一個辦法是將自己的類實現為Externalizable,就象前面展示的那樣。這樣一來,沒有任何東西可以自動序列化,只能在writeExternal()明確序列化那些需要的部分。
          然而,若操作的是一個Serializable對象,所有序列化操作都會自動進行。為解決這個問題,可以用transient(臨時)逐個字段地關閉序列化,它的意思是“不要麻煩你(指自動機制)保存或恢復它了——我會自己處理的”。
          例如,假設一個Login對象包含了與一個特定的登錄會話有關的信息。校驗登錄的合法性時,一般都想將數據保存下來,但不包括密碼。為做到這一點,最簡單的辦法是實現Serializable,并將password字段設為transient。下面是具體的代碼:

           

          //: Logon.java
          // Demonstrates the "transient" keyword
          import java.io.*;
          import java.util.*;

          class Logon implements Serializable {
            
          private Date date = new Date();
            
          private String username;
            
          private transient String password;
            Logon(String name, String pwd) 
          {
              username 
          = name;
              password 
          = pwd;
            }

            
          public String toString() {
              String pwd 
          =
                (password 
          == null? "(n/a)" : password;
              
          return "logon info: \n   " +
                
          "username: " + username +
                
          "\n   date: " + date.toString() +
                
          "\n   password: " + pwd;
            }

            
          public static void main(String[] args) {
              Logon a 
          = new Logon("Hulk""myLittlePony");
              System.out.println( 
          "logon a = " + a);
              
          try {
                ObjectOutputStream o 
          =
                  
          new ObjectOutputStream(
                    
          new FileOutputStream("Logon.out"));
                o.writeObject(a);
                o.close();
                
          // Delay:
                int seconds = 5;
                
          long t = System.currentTimeMillis()
                       
          + seconds * 1000;
                
          while(System.currentTimeMillis() < t)
                  ;
                
          // Now get them back:
                ObjectInputStream in =
                  
          new ObjectInputStream(
                    
          new FileInputStream("Logon.out"));
                System.out.println(
                  
          "Recovering object at " + new Date());
                a 
          = (Logon)in.readObject();
                System.out.println( 
          "logon a = " + a);
              }
           catch(Exception e) {
                e.printStackTrace();
              }

            }

          }
           ///:~



          可以看到,其中的date和username字段保持原始狀態(未設成transient),所以會自動序列化。然而,password被設為transient,所以不會自動保存到磁盤;另外,自動序列化機制也不會作恢復它的嘗試。輸出如下:

          logon a = logon info:
             username: Hulk
             date: Sun Mar 23 18:25:53 PST 1997
             password: myLittlePony
          Recovering object at Sun Mar 23 18:25:59 PST 1997
          logon a = logon info:
             username: Hulk
             date: Sun Mar 23 18:25:53 PST 1997
             password: (n/a)一旦對象恢復成原來的樣子,password字段就會變成null。注意必須用toString()檢查password是否為null,因為若用過載的“+”運算符來裝配一個String對象,而且那個運算符遇到一個null句柄,就會造成一個名為NullPointerException的違例(新版Java可能會提供避免這個問題的代碼)。
          我們也發現date字段被保存到磁盤,并從磁盤恢復,沒有重新生成。
          由于Externalizable對象默認時不保存它的任何字段,所以transient關鍵字只能伴隨Serializable使用。

          2. Externalizable的替代方法
          若不是特別在意要實現Externalizable接口,還有另一種方法可供選用。我們可以實現Serializable接口,并添加(注意是“添加”,而非“覆蓋”或者“實現”)名為writeObject()和readObject()的方法。一旦對象被序列化或者重新裝配,就會分別調用那兩個方法。也就是說,只要提供了這兩個方法,就會優先使用它們,而不考慮默認的序列化機制。
          這些方法必須含有下列準確的簽名:

          private void
            writeObject(ObjectOutputStream stream)
              throws IOException;

          private void
            readObject(ObjectInputStream stream)
              throws IOException, ClassNotFoundException從設計的角度出發,情況變得有些撲朔迷離。首先,大家可能認為這些方法不屬于基礎類或者Serializable接口的一部分,它們應該在自己的接口中得到定義。但請注意它們被定義成“private”,這意味著它們只能由這個類的其他成員調用。然而,我們實際并不從這個類的其他成員中調用它們,而是由ObjectOutputStream和ObjectInputStream的writeObject()及readObject()方法來調用我們對象的writeObject()和readObject()方法(注意我在這里用了很大的抑制力來避免使用相同的方法名——因為怕混淆)。大家可能奇怪ObjectOutputStream和ObjectInputStream如何有權訪問我們的類的private方法——只能認為這是序列化機制玩的一個把戲。
          在任何情況下,接口中的定義的任何東西都會自動具有public屬性,所以假若writeObject()和readObject()必須為private,那么它們不能成為接口(interface)的一部分。但由于我們準確地加上了簽名,所以最終的效果實際與實現一個接口是相同的。
          看起來似乎我們調用ObjectOutputStream.writeObject()的時候,我們傳遞給它的Serializable對象似乎會被檢查是否實現了自己的writeObject()。若答案是肯定的是,便會跳過常規的序列化過程,并調用writeObject()。readObject()也會遇到同樣的情況。
          還存在另一個問題。在我們的writeObject()內部,可以調用defaultWriteObject(),從而決定采取默認的writeObject()行動。類似地,在readObject()內部,可以調用defaultReadObject()。下面這個簡單的例子演示了如何對一個Serializable對象的存儲與恢復進行控制:


           

          //: SerialCtl.java
          // Controlling serialization by adding your own
          // writeObject() and readObject() methods.
          import java.io.*;

          public class SerialCtl implements Serializable {
            String a;
            
          transient String b;
            
          public SerialCtl(String aa, String bb) {
              a 
          = "Not Transient: " + aa;
              b 
          = "Transient: " + bb;
            }

            
          public String toString() {
              
          return a + "\n" + b;
            }

            
          private void 
              writeObject(ObjectOutputStream stream)
                
          throws IOException {
              stream.defaultWriteObject();
              stream.writeObject(b);
            }

            
          private void 
              readObject(ObjectInputStream stream)
                
          throws IOException, ClassNotFoundException {
              stream.defaultReadObject();
              b 
          = (String)stream.readObject();
            }

            
          public static void main(String[] args) {
              SerialCtl sc 
          = 
                
          new SerialCtl("Test1""Test2");
              System.out.println(
          "Before:\n" + sc);
              ByteArrayOutputStream buf 
          = 
                
          new ByteArrayOutputStream();
              
          try {
                ObjectOutputStream o 
          =
                  
          new ObjectOutputStream(buf);
                o.writeObject(sc);
                
          // Now get it back:
                ObjectInputStream in =
                  
          new ObjectInputStream(
                    
          new ByteArrayInputStream(
                      buf.toByteArray()));
                SerialCtl sc2 
          = (SerialCtl)in.readObject();
                System.out.println(
          "After:\n" + sc2);
              }
           catch(Exception e) {
                e.printStackTrace();
              }

            }

          }
           ///:~


          在這個例子中,一個String保持原始狀態,其他設為transient(臨時),以便證明非臨時字段會被defaultWriteObject()方法自動保存,而transient字段必須在程序中明確保存和恢復。字段是在構建器內部初始化的,而不是在定義的時候,這證明了它們不會在重新裝配的時候被某些自動化機制初始化。
          若準備通過默認機制寫入對象的非transient部分,那么必須調用defaultWriteObject(),令其作為writeObject()中的第一個操作;并調用defaultReadObject(),令其作為readObject()的第一個操作。這些都是不常見的調用方法。舉個例子來說,當我們為一個ObjectOutputStream調用defaultWriteObject()的時候,而且沒有為其傳遞參數,就需要采取這種操作,使其知道對象的句柄以及如何寫入所有非transient的部分。這種做法非常不便。
          transient對象的存儲與恢復采用了我們更熟悉的代碼?,F在考慮一下會發生一些什么事情。在main()中會創建一個SerialCtl對象,隨后會序列化到一個ObjectOutputStream里(注意這種情況下使用的是一個緩沖區,而非文件——與ObjectOutputStream完全一致)。正式的序列化操作是在下面這行代碼里發生的:
          o.writeObject(sc);
          其中,writeObject()方法必須核查sc,判斷它是否有自己的writeObject()方法(不是檢查它的接口——它根本就沒有,也不是檢查類的類型,而是利用反射方法實際搜索方法)。若答案是肯定的,就使用那個方法。類似的情況也會在readObject()上發生?;蛟S這是解決問題唯一實際的方法,但確實顯得有些古怪。

          3. 版本問題
          有時候可能想改變一個可序列化的類的版本(比如原始類的對象可能保存在數據庫中)。盡管這種做法得到了支持,但一般只應在非常特殊的情況下才用它。此外,它要求操作者對背后的原理有一個比較深的認識,而我們在這里還不想達到這種深度。JDK 1.1的HTML文檔對這一主題進行了非常全面的論述(可從Sun公司下載,但可能也成了Java開發包聯機文檔的一部分)。

          posted on 2008-05-28 12:00 vesung 閱讀(680) 評論(0)  編輯  收藏 所屬分類: Java
          主站蜘蛛池模板: 中西区| 宾阳县| 汉源县| 卫辉市| 岐山县| 辽宁省| 施甸县| 安陆市| 敦煌市| 嘉峪关市| 旬邑县| 乾安县| 琼中| 曲周县| 麟游县| 安顺市| 福鼎市| 日照市| 甘谷县| 陆良县| 辽阳县| 内丘县| 社旗县| 台前县| 南涧| 清远市| 淳化县| 平陆县| 沂源县| 翁牛特旗| 汉寿县| 贡觉县| 思茅市| 庄浪县| 长寿区| 泸西县| 年辖:市辖区| 象州县| 澄江县| 铜梁县| 通州市|