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。