JavaRush /Java Blog /Random-TW /序列化就是這樣。第1部分
articles
等級 15

序列化就是這樣。第1部分

在 Random-TW 群組發布
乍一看,序列化似乎是一個微不足道的過程。真的,還有什麼可以更簡單呢?聲明類別來實作介面java.io.Serializable- 就是這樣。您可以毫無問題地序列化​​該類別。 序列化就是這樣。 第 1 - 1 部分從理論上講,這是正確的。在實踐中,有很多微妙之處。它們與效能、反序列化和類別安全性相關。還有更多方面。我們將討論這些微妙之處。本文可分為以下幾個部分:
  • 機制的微妙之處
  • 為什麼需要它?Externalizable
  • 表現
  • 但另一方面
  • 資料安全
  • 物件序列化Singleton
讓我們繼續第一部分——

機制的微妙之處

首先,一個簡單的問題。有多少種方法可以使物件可序列化?實踐表明,90%以上的開發者以大致相同的方式回答這個問題(就措辭而言)——只有一種方法。同時,他們中有兩個人。並不是每個人都記得第二個,更不用說談論它的功能了。那麼這些方法有哪些呢?每個人都記得第一個。這是已經提到的實現java.io.Serializable,不需要任何努力。第二種方法也是介面的實現,但是不同:java.io.Externalizable。不同的是java.io.Serializable,它包含兩個需要實現的方法 -writeExternal(ObjectOutput)readExternal(ObjectInput)。這些方法包含序列化/反序列化邏輯。 評論。Serializable在下文中,我有時會提到序列化,其中實作是標準的,實作Externalizable是擴展的。其他評論。我現在故意不觸及諸如定義readObject和之類的標準序列化控制選項writeObject,因為 我認為這些方法有些不正確。這些方法沒有在介面中定義Serializable,實際上是解決限制並使標準序列化變得靈活的道具。Externalizable提供靈活性的方法從一開始就內建在它們中。我們再問一個問題。標準序列化實際上是如何運作的java.io.Serializable?它透過 Reflection API 工作。那些。該類別被解析為一組字段,每個字段都寫入輸出流。我認為很明顯,這種操作在性能方面並不是最佳的。稍後我們會知道具體是多少。提到的兩種序列化方法之間還有另一個主要差異。即在反序列化機制中。使用時,Serializable反序列化是這樣發生的:為物件分配內存,然後用流中的值填充其字段。不呼叫該物件的建構函數。這裡我們需要單獨考慮這種情況。好的,我們的類別是可序列化的。他的父母呢?完全可選!此外,如果您從Object父級繼承一個類,那麼父級絕對是不可序列化的。儘管Object我們對字段一無所知,但它們很可能存在於我們自己的父類別中。他們會發生什麼事?它們不會進入序列化流。反序列化後它們會取什麼值?讓我們來看這個例子:
package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
它是透明的-我們有一個不可序列化的父類別和一個可序列化的子類別。這就是發生的事情:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
反序列化時,呼叫父非序列化類別的無參數建構子。而如果沒有這樣的建構函數,反序列化時就會出錯。如同上面所說,我們正在反序列化的子物件的建構子不會被呼叫。這就是標準機制在使用時的行為Serializable。使用時,Externalizable情況有所不同。首先,呼叫不帶參數的建構函數,然後在建立的物件上呼叫 readExternal 方法,該方法實際上讀取其所有資料。因此,任何實作Externalized介面的類別都必須有一個不帶參數的公共建構函式!而且,由於此類的所有後代也將被視為實作該接口Externalizable,因此它們也必須有一個無參數建構函式!讓我們更進一步。有這樣一個字段修飾符,如transient。這意味著該字段不應被序列化。然而,正如您自己所理解的,該指令僅影響標準序列化機制。使用時,Externalizable沒有人費心序列化該字段以及減去它。如果一個欄位被宣告為瞬態的,那麼當該物件被反序列化時,它將採用預設值。另一個相當微妙的點。使用標準序列化時,具有修飾符的欄位static不會被序列化。因此,反序列化後該欄位的值不會改變。當然,在實現過程中,Externalizable沒有人費心去序列化和反序列化這個字段,但我強烈建議不要這樣做,因為 這可能會導致細微的錯誤。帶有修飾符的欄位final像常規欄位一樣被序列化。但有一個例外 - 使用外部化時無法反序列化它們。因為final-поля它們必須在建構函式中初始化,之後就無法在 readExternal 中更改該欄位的值。因此,如果您需要序列化具有 -field 的對象final,則只需使用標準序列化。還有一點很多人不知道。標準序列化考慮了類別中聲明欄位的順序。無論如何,早期版本就是這種情況;在 Oracle 實作的 JVM 版本 1.6 中,順序不再重要,欄位的類型和名稱很重要。儘管這些字段通常可能保持不變,但方法的組成很可能會影響標準機制。為了避免這種情況,有以下機制。對於實現該介面的每個類Serializable,在編譯階段都會新增一個欄位 -private static final long serialVersionUID。此欄位包含序列化類別的唯一版本識別碼。它是根據類別的內容計算的 - 欄位、它們的聲明順序、方法、它們的聲明順序。因此,隨著類別的任何變化,該欄位的值也會改變。當類別被序列化時,該欄位被寫入流。順便說一句,這可能是我所知道的唯一static一個 -field 被序列化的情況。反序列化時,將該欄位的值與虛擬機器中類別的值進行比較。如果值不匹配,則會拋出如下異常:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
然而,有一種方法(如果不能繞過的話)可以欺騙此檢查。如果已經定義了類別欄位集及其順序,但類別方法可能會更改,則這可能很有用。在這種情況下,序列化沒有風險,但標準機制不允許使用修改後的類別的字節碼來反序列化資料。但是,正如我所說,他可能會被欺騙。即手動定義類別中的欄位private static final long serialVersionUID。原則上,該欄位的值絕對可以是任何值。有些人喜歡將其設定為等於程式碼修改的日期。有的甚至用1L。若要取得標準值(內部計算的值),您可以使用 SDK 中包含的 Serialver 公用程式。一旦以這種方式定義,該欄位的值將是固定的,因此始終允許反序列化。此外,在5.0 版本中,文件中大約出現了以下內容:強烈建議所有可序列化類別明確聲明此字段,因為預設計算對類別結構的細節非常敏感,這些細節可能會根據編譯器實現而有所不同,從而導致意想不到的InvalidClassException後果。反序列化。最好將此欄位聲明為private,因為 它僅指聲明它的類別。儘管規範中沒有指定修飾符。現在讓我們考慮這個方面。假設我們有這樣的類別結構:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
換句話說,我們有一個從不可序列化父級繼承的類別。是否可以序列化此類,並且需要什麼?父類別的變數會發生什麼變化呢?答案是這樣的。是的,B您可以序列化類別的實例。為此需要什麼?但該類別需要A有一個不帶參數的建構函數,publicprotected. 然後,在反序列化期間,所有類別變數A將使用此構造函數進行初始化。類別變數B將使用序列化資料流中的值進行初始化。理論上,可以在類別中定義B我在開頭 -readObject和, - 開頭談到的方法,透過writeObject執行類別變數的(反)序列化,然後對可用變數進行(反)序列化來自類別(在我們的例子中,它們是、和,如果它與 位於同一個包中)。然而,在我看來,最好使用擴展序列化。我想談的下一點是多個物件的序列化。假設我們有以下類別結構: Bin.defaultReadObject/out.defaultWriteObjectAiPubliciProtectediPackageBA
public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
序列化就是這樣。 第 1 - 2 部分如果序列化該類別的實例會發生什麼A?它將沿著類別的一個實例拖動B,類別的實例又將沿著一個C引用該實例的實例拖動A,該實例與一切開始時的實例相同。惡性循環和無限遞歸?幸運的是,沒有。我們來看下面的測試程式碼:
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
我們在做什麼?A我們創建類別、B和的實例C,為它們提供相互鏈接,然後序列化它們中的每一個。然後我們將它們反序列化並運行一系列檢查。結果會發生什麼:
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
那你能從這個測驗中學到什麼?第一的。反序列化後的物件引用與先前的引用不同。換句話說,在序列化/反序列化期間,物件被複製。此方法有時用於克隆物件。第二個結論更有意義。當序列化/反序列化具有交叉引用的多個物件時,這些引用在反序列化後仍然有效。換句話說,如果在序列化之前它們指向一個對象,那麼在反序列化之後它們也將指向一個對象。另一個小測試來證實這一點:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
類別物件B具有對類別物件的參考C。序列化時,b它與 類別 的實例一起序列化С,之後同一個 c 實例被序列化 3 次。反序列化後會發生什麼事?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
正如您所看到的,所有四個反序列化物件實際上代表一個物件 - 對它的引用是相等的。與連載之前一模一樣。Externalizable另一個有趣的點 - 如果我們同時實現和會發生什麼Serializable?就像那個問題──大象與鯨魚──誰會打敗誰?會克服的Externalizable。序列化機制首先檢查它是否存在,然後才檢查它是否存在。Serializable因此,如果實作 的類別 BSerializable繼承自實作 的類別 A,則Externalizable類別 B 的欄位將不會被序列化。最後一點是繼承。當從實作 的類別繼承時Serializable,無需執行任何其他操作。序列化也將擴展到子類別。當從實作 的類別繼承時Externalizable,必須重寫父類別的 readExternal 和 writeExternal 方法。否則,子類別的欄位將不會被序列化。在這種情況下,您應該記住呼叫父方法,否則父欄位將不會被序列化。* * * 我們可能已經完成了細節。然而,有一個我們尚未觸及的全球性問題。即——

為什麼需要外部化?

為什麼我們需要高階序列化?答案很簡單。首先,它提供了更大的靈活性。其次,它通常可以在序列化資料量方面提供顯著的效益。第三,還有一個方面,就是性能,我們下面會講到。一切似乎都清晰又靈活。確實,我們可以根據需要控制序列化和反序列化過程,這使我們獨立於類別中的任何變更(正如我上面所說,類別中的變更可以極大地影響反序列化)。因此,我想就成交量的增益說幾句話。假設我們有以下課程:
public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
其餘的都不重要。這些欄位可以由 int 類型組成,但這只會增強範例的效果。儘管實際上這些欄位可能int出於效能原因而被鍵入。無論如何,這一點是明確的。該類別代表日期和時間。主要從序列化的角度來看,這對我們來說很有趣。也許最簡單的就是儲存一個簡單的時間戳記。它的類型是 long,即 序列化時需要 8 個位元組。此外,這種方法需要將元件轉換為一個值並傳回的方法,即 – 生產力損失。這種方法的優點是可以容納 64 位元的完全瘋狂的日期。這是一個巨大的安全邊際,但現實中通常不需要。上面給出的類別將佔用 2 + 5*1 = 7 個位元組。加上類別和 6 個欄位的開銷。有什麼辦法可以壓縮這些數據嗎?一定。秒和分鐘的範圍是 0-59,即 為了表示它們,6 位就足夠了,而不是 8 位。小時 – 0-23(5 位),天 – 0-30(5 位),月份 – 0-11(4 位)。總計,不考慮年份的所有內容 - 26 位。int 的大小還剩 6 位。理論上,在某些情況下,這可能足夠一年。如果不是,新增另一個位元組會將資料欄位的大小增加到 14 位,這給出了 0-16383 的範圍。這在實際應用上已經足夠了。總的來說,我們已將儲存必要資訊所需的資料大小減少到 5 個位元組。如果不超過4。缺點和前一種情況一樣-如果你儲存的是打包的日期,那麼就需要轉換方法。但我想這樣做:將其存儲在單獨的字段中並以打包形式序列化。這是使用有意義的地方Externalizable
// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
事實上,僅此而已。序列化後,我們得到每個類別的開銷、兩個欄位(而不是 6 個)和 5 個位元組的資料。這已經明顯更好了。進一步的包裝可以留給專門的圖書館。給出的例子非常簡單。其主要目的是展示如何使用高級序列化。儘管在我看來,序列化資料量的可能增加遠不是主要優勢。主要優點,除了靈活性之外......(順利地進入下一節......)連結到來源:序列化原樣
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION