1. 程式人生 > >Java物件為什麼要事先序列化介面

Java物件為什麼要事先序列化介面

客戶端訪問了某個能開啟會話功能的資源, web伺服器就會建立一個與該客戶端對應的HttpSession物件,每個HttpSession物件都要站用一定的記憶體空間。如果在某一時間段內訪問站點的使用者很多,web伺服器記憶體中就會積累大量的HttpSession物件,消耗大量的伺服器記憶體,即使使用者已經離開或者關閉了瀏覽器,web伺服器仍要保留與之對應的HttpSession物件,在他們超時之前,一直佔用web伺服器記憶體資源。

 web伺服器通常將那些暫時不活動但未超時的HttpSession物件轉移到檔案系統或資料庫中儲存,伺服器要使用他們時再將他們從檔案系統或資料庫中裝載入記憶體,這種技術稱為Session的持久化。

  將HttpSession物件儲存到檔案系統或資料庫中,需要採用序列化的方式將HttpSession物件中的每個屬性物件儲存到檔案系統或資料庫中;將HttpSession物件從檔案系統或資料庫中裝載如記憶體時,需要採用反序列化的方式,恢復HttpSession物件中的每個屬性物件。所以儲存在HttpSession物件中的每個屬性物件必須實現Serializable介面。

serialVersionUID 的作用

serialVersionUID 用來表明類的不同版本間的相容性Java的序列化機制是通過在執行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的位元組流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常。 當實現java.io.Serializable介面的實體(類)沒有顯式地定義一個名為serialVersionUID,型別為long的變數時,Java序列化機制會根據編譯的class自動生成一個serialVersionUID作序列化版本比較用,這種情況下,只有同一次編譯生成的class才會生成相同的serialVersionUID 。 如果我們不希望通過編譯來強制劃分軟體版本,即實現序列化介面的實體能夠相容先前版本,未作更改的類,就需要顯式地定義一個名為serialVersionUID,型別為long的變數,不修改這個變數值的序列化實體都可以相互進行序列化和反序列化。

引起這個疑問,還是從Hibernate使用查詢快取說起;物件例項除了存在於記憶體,二級快取還會將物件寫進硬碟在需要的時候再讀取出來使用,此時就必須提到一個概念:序列化。

       程式在執行時例項化出物件,這些物件存在於記憶體中,隨著程式執行停止而消失,但如果我們想把某些物件(一般都是各不相同的屬性)儲存下來或者傳輸給其他程序,在程式終止執行後這些物件仍然存在,可以在程式再次執行時讀取這些物件的資訊,或者在其他程式中利用這些儲存下來的物件資訊恢復成例項物件。這種情況下就要用到物件序列化和反序列化

       其實很早就知道的,在Java中常見的幾個類,如:Interger/String等,都實現了java.io.Serializable介面。這個序列化介面沒有任何方法和域,僅用於標識序列化語意;實現 Serializable 介面的類是可序列化的,沒有實現此介面的類將不能被序列化和反序列化。序列化類的所有子類本身都是可序列化的,不再需要顯式實現 Serializable 介面。只有經過序列化,才能相容物件在磁碟文字以及在網路中的傳輸,以及恢復物件的時候反序列化等操作。

問題一:為何要實現序列化?

答:序列化就是對例項物件的狀態(State 物件屬性而不包括物件方法)進行通用編碼(如格式化的位元組碼)並儲存,以保證物件的完整性和可傳遞性。

簡而言之:序列化,就是為了在不同時間或不同平臺的JVM之間共享例項物件

// 經常使用如下:
public static void main(String[] args) throws Exception {
    File file = new File("user.ser");
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
User user = new User("zhang", 18, Gender.MALE);
oout.writeObject(user);
oout.close();

ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newUser = oin.readObject();
oin.close();
System.out.println(newUser);

}

       如沒有 實現Serializable介面,在序列化時,使用ObjectOutputStream的write(object)方法將物件儲存時將會出現異常。其實 java.io.Serializable 只是一個沒有屬性和方法的空介面,但是問題來了。。。

問題二:為何一定要實現 Serializable 才能進行序列化呢?

使用 ObjectOutputStream 來持久化物件, 對於此處丟擲的異常,檢視該類中實現如下:

private void writeObject0(Object obj, boolean unshared) throws IOException {
    // ...
            // remaining cases
            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
    // ...
}
 從此可知, 如果被寫物件型別是String、陣列、Enum、Serializable,就可以進行序列化,否則將丟擲NotSerializableException。

最後提點注意:

1、在序列化物件時,不僅會序列化當前物件本身,還會對該物件引用的其它物件也進行序列化,如此引用傳遞序列化。如果一個物件包含的成員變數是容器類等並深層引用,那麼序列化過程開銷也較大。

2、當欄位被宣告為 transient 後,預設序列化機制就會忽略該欄位。(還有方法就是自定義writeObject方法,見下程式碼示例)

3、在單例類中新增一個readResolve()方法(直接返回單例物件),以保證在序列化過程仍保持單例特性。

此外補充一下,

    在路徑下jdk中還有另外一種形式的物件持久化,即:外部化(Externalization)。

public interface Externalizable extends java.io.Serializable {
  void writeExternal(ObjectOutput out) throws IOException;
  void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

    外部化和序列化是實現同一目標的兩種不同方法。

       通過 Serializable 介面對物件序列化的支援是jdk內支援的 API ,但是java.io.Externalizable的所有實現者必須提供讀入和寫出的具體實現,怎麼實現完全由你自定義。序列化(Serializable )會自動儲存所有必要的資訊(如屬性以及屬性型別等),用以反序列化成原來一樣的例項,而外部化(Externalizable)則只儲存被儲存例項中你需要的資訊。

示例程式碼如下:

public class User implements Externalizable {
    private String name;
    transient private Integer age;  // 遮蔽欄位
    private Gender gender;
<span>public User</span>() {
    System.out.println("none constructor");
}

public User(String name, Integer age, Gender gender) {
    System.out.println("arg constructor");
    this.name = name;
    this.age = age;
    this.gender = gender;
}

// 實現讀寫
private void <span>writeObject</span>(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
    out.writeInt(age);
    // 遮蔽gender
}
private void <span>readObject</span>(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    age = in.readInt();
}

// 具體重寫
@Override
public void <span>writeExternal</span>(ObjectOutput out) throws IOException {
    out.writeObject(name);
    out.writeInt(age);
    // 遮蔽gender
}
@Override
public void <span>readExternal</span>(ObjectInput in) throws IOException, ClassNotFoundException {
    name = (String) in.readObject();
    age = in.readInt();
} 

} 注意,用Externalizable進行序列化,當讀取物件時,會呼叫被序列化類的無參構造器建立一個新的物件,然後再將被儲存物件的欄位的值分別填充到新物件中。實現Externalizable介面的類必須要提供一個無參的構造器,且訪問許可權為 public。