當兩個進程在進行遠程通信時,彼此可以發送各種類型的數據。無論是何種類型的數據,都會以二進制序列的形式在網絡上傳送。發送方需要把這個Java對象轉換為字節序列,才能在網絡上傳送;接收方則需要把字節序列再恢復為Java對象。
把Java對象轉換為字節序列的過程稱為對象的序列化。
把字節序列恢復為Java對象的過程稱為對象的反序列化。
-----------以下內容節選自《Thinking in java 3rd Edition》-------------
利用對象序列化可以實現“輕量級持久化”(lightweight persistence)。“持久化”意味著一個對象的生存周期并不取決于程序是否正在執行;它可以生存于程序的調用之間。通過將一個序列化對象寫入磁盤,然后在重新調用時恢復該對象,就能夠實現持久化的效果。之所以稱其為“輕量級”,是因為不能用某種“persistent”(持久)關鍵字來簡單地定義一個對象,并讓系統自動維護其他細節問題(盡管將來有可能實現)。相反,對象必須在程序中顯式地序列化和重組。如果需要一個更嚴格的持久化機制,可以考慮使用Java數據對象(JDO)或者像Hibernate之類的工具
對象序列化的概念加入到語言中是為了提供對兩種主要特性的支持:
·Java的“遠程方法調用”(RMI,Remote Method Invocation)使存活于其他計算機上的對象使用起來就像是存活于本機上一樣。當向遠程對象發送消息時,需要通過對象序列化來傳輸參數和返回值。
·對Java Beans來說對象序列化也是必需的。使用一個Bean時,一般情況下是在設計階段對它的狀態信息進行配置。這種狀態信息必須保存下來,并在程序啟動以后,進行恢復;具體工作由對象序列化完成。 ----------------------------------------------- 對象的序列化主要有兩種用途:
1) 把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中;
2) 在網絡上傳送對象的字節序列。
一. JDK類庫中的序列化API java.io.ObjectOutputStream代表對象輸出流,它的writeObject(Object obj)方法可對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中。
java.io.ObjectInputStream代表對象輸入流,它的readObject()方法從一個源輸入流中讀取字節序列,再把它們反序列化為一個對象,并將其返回。
只有實現了Serializable和Externalizable接口的類的對象才能被序列化。Externalizable接口繼承自Serializable接口,實現Externalizable接口的類完全由自身來控制序列化的行為,而僅實現Serializable接口的類可以采用默認的序列化方式 。
對象序列化包括如下步驟:
1) 創建一個對象輸出流,它可以包裝一個其他類型的目標輸出流,如文件輸出流;
2) 通過對象輸出流的writeObject()方法寫對象。
對象反序列化的步驟如下:
1) 創建一個對象輸入流,它可以包裝一個其他類型的源輸入流,如文件輸入流;
2) 通過對象輸入流的readObject()方法讀取對象。
下面讓我們來看一個對應的例子,類的內容如下:
輸出結果如下:
obj1=你好!
obj2=Sat Sep 15 22:02:21 CST 2007
obj3=name=阿蜜果, age=24
obj4=123
因此例比較簡單,在此不再詳述。
二 實現Serializable接口
ObjectOutputStream只能對Serializable接口的類的對象進行序列化。默認情況下,ObjectOutputStream按照默認方式序列化,這種序列化方式僅僅對對象的非transient的實例變量進行序列化,而不會序列化對象的transient的實例變量,也不會序列化靜態變量。 當ObjectIntputStream按照默認方式反序列化時,具有如下特點:
1) 如果在內存中對象所屬的類還沒有被加載,那么會先加載并初始化這個類。如果在classpath中不存在相應的類文件,那么會拋出ClassNotFoundException;
2) 在反序列化時不會調用類的任何構造方法(注意與下面Externalizable接口的區別)。
如果用戶希望控制類的序列化方式,可以在可序列化類中提供以下形式的writeObject()和readObject()方法。
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
需要注意的地方是:上面兩個方法并不是Serializable接口定義的,Serializable只是一個標記接口,并沒有任何內容。而且這兩個方法都是private的,但卻并不是被定義這兩個方法的類本身所調用——當ObjectOutputStream對一個Customer對象進行序列化時,如果該對象具有writeObject()方法,那么就會執行這一方法,否則就按默認方式序列化。在該對象的writeObjectt()方法中,可以先調用ObjectOutputStream的defaultWriteObject()方法,使得對象輸出流先執行默認的序列化操作。同理可得出反序列化的情況,不過這次是defaultReadObject()方法。
有些對象中包含一些敏感信息,這些信息不宜對外公開。如果按照默認方式對它們序列化,那么它們的序列化數據在網絡上傳輸時,可能會被不法份子竊取。對于這類信息,可以對它們進行加密后再序列化,在反序列化時則需要解密,再恢復為原來的信息——這是transient關鍵字的第一個用途,屏蔽敏感信息。
transient的第二個功能是在用途時,將某些無需序列化的成員變量設為transient類型,將節省空間和時間,提高序列化的性能。 transient的第三個用途是如果類的內部有某個非序列化的對象引用,可以將其標記為transient來避免拋出NotSerializableException異常 默認的序列化方式會序列化整個對象圖,這需要遞歸遍歷對象圖。如果對象圖很復雜,遞歸遍歷操作需要消耗很多的空間和時間,它的內部數據結構為雙向列表。
注意在遞歸遍歷過程中,對同一對象的引用如果出現多次,序列化過程并不會重復寫入多個,具體的做法如下: ·保存到磁盤的所有對象都獲得一個序列號(1、2、3等) ·當要保存一個對象時,先檢查該對象是否已經被保存了 ·如果以前保存過,只需寫入“與已經保存的具有序列號x的對象相同”標記;否則,保存它的所有數據 當需要讀回對象時,將上述過程簡單地逆轉即可。
三 實現Externalizable接口
Externalizable接口繼承自Serializable接口,如果一個類實現了Externalizable接口,那么將完全由這個類控制自身的序列化行為。Externalizable接口聲明了兩個方法:
public void writeExternal(ObjectOutput out) throws IOException
public void readExternal(ObjectInput in) throws IOException , ClassNotFoundException
前者負責序列化操作,后者負責反序列化操作。
在對實現了Externalizable接口的類的對象進行反序列化時,會先調用類的不帶參數的構造方法,這是有別于默認反序列方式的。如果把類的不帶參數的構造方法刪除,或者把該構造方法的訪問權限設置為private、默認或protected級別,會拋出java.io.InvalidException: no valid constructor異常。 類實現externalizable時,頭寫入對象流中,然后類完全負責序列化和恢復數據成員,除了頭以外,根本沒有自動序列化。 這里要注意了:聲明類實現Externalizable接口會有重大的安全風險。writeExternal()與readExternal()方法聲明為public,惡意類可以用這些方法讀取和寫入對象數據。如果對象包含敏感信息,則要格外小心。這包括使用安全套接或加密整個字節流。
四 可序列化類的不同版本的序列化兼容性 凡是實現Serializable接口的類都有一個表示序列化版本標識符的靜態變量: private static final long serialVersionUID; 以上serialVersionUID的取值是Java運行時環境根據類的內部細節自動生成的。如果對類的源代碼作了修改,再重新編譯,新生成的類文件的serialVersionUID的取值有可能也會發生變化。
類的serialVersionUID的默認值完全依賴于Java編譯器的實現,對于同一個類,用不同的Java編譯器編譯,有可能會導致不同的serialVersionUID,也有可能相同。為了提高serialVersionUID的獨立性和確定性,強烈建議在一個可序列化類中顯示的定義serialVersionUID,為它賦予明確的值。顯式地定義serialVersionUID有兩種用途:
1) 在某些場合,希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有相同的serialVersionUID;
2) 在某些場合,不希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有不同的serialVersionUID。
這兩個本來封裝的較好的咚咚也不能得到保障了
2、Serializable會為每個類生成一個序列號,生成依據是類名、類實現的接口名、
public和protected方法,所以只要你一不小心改了一個已經publish的API,并且沒有自
己定義一個long類型的叫做serialVersionUID的field,哪怕只是添加一個getXX,就會
讓你讀原來的序列化到文件中的東西讀不出來(不知道為什么要把方法名算進去?)
3、不用構造函數用Serializable就可以構造對象,看起來不大合理,這被稱為
extralinguistic mechanism,所以當實現Serializable時應該注意維持構造函數中所維
持的那些不變狀態
4、增加了發布新版本的類時的測試負擔
5、1.4版本后,JavaBeans的持久化采用基于XML的機制,不再需要Serializable
6、設計用來被繼承的類時,盡量不實現Serializable,用來被繼承的interface也不要
繼承Serializable。但是如果父類不實現Serializable接口,子類很難實現它,特別是
對于父類沒有可以訪問的不含參數的構造函數的時候。所以,一旦你決定不實現
Serializable接口并且類被用來繼承的時候記得提供一個無參數的構造函數
7、不管你選擇什么序列化形式,聲明一個顯式的UID:
private static final long serialVersionUID = randomLongValue;
8、不需要序列化的東西使用transient注掉它吧,別什么都留著
9、writeObject/readObject重載以完成更好的序列化
readResolve 與 writeReplace重載以完成更好的維護invariant controllers