JAVA中序列化和還原序列化中的靜態成員問題
關於這個標題的內容是面試筆試中比較常見的考題,大家跟隨我的部落格一起來學習下這個過程。
JAVA中的序列化和還原序列化主要用於:
(1)將對象或者異常等寫入檔案,通過檔案互動傳輸資訊; (2)將對象或者異常等通過網路進行傳輸。
那麼為什麼需要序列化和還原序列化呢。簡單來說,如果你只是自己同一台機器的同一個環境下使用同一個JVM來操作,序列化和還原序列化是沒必要的,當需要進行資料轉送的時候就顯得十分必要。比如你的資料寫到檔案裡要被其他人的電腦的程式使用,或者你電腦上的資料需要通過網路傳輸給其他人的程式使用,像伺服器用戶端的這種模型就是一種應用,這個時候,大家想想,每個人的電腦配置可能不同,運行環境可能也不同,位元組序可能也不同,總之很多地方都不能保證一致,所以為了統一起見,我們傳輸的資料或者經過檔案儲存的資料需要經過序列化和編碼等操作,相當於互動雙方有一個公用的標準,按照這種標準來做,不管各自的環境是否有差異,各自都可以根據這種標準來翻譯出自己能理解的正確的資料。
在JAVA中有專門用於此類操作的API,供開發人員直接使用,對象的序列化和還原序列化可以通過將對象實現Serializable介面,然後用對象的輸入輸出資料流進行讀寫,下面看一個完整的例子。
package test2;import java.io.Serializable;public class DataObject implements Serializable {/** * 序列化的UID號 */private static final long serialVersionUID = -3737338076212523007L;public static int i = 0;private String word = "";public static void setI(int i){DataObject.i = i;}public void setWord(String word){this.word = word;}public static int getI() {return i;}public String getWord() {return word;}@Overridepublic String toString() {return "word = " + word + ", " + "i = " + i;}}
上面這段程式是定義了要被序列化和還原序列化的類DataObject,這個類實現了Serializable介面,裡面有幾點需要注意:
(1)類中有一個靜態成員變數i,這個變數能不能被序列化呢。等下通過測試程式看一下; (2)類中重寫了toString方法,是為了列印結果。
接下來我們看一下測試該類的對象序列化和還原序列化的一個測試程式版本,提前說明,這個版本是有問題的。
package test2;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;/** * Description: 測試對象的序列化和反序列 */public class TestObjSerializeAndDeserialize { public static void main(String[] args) throws Exception { // 序列化DataObject對象 Serialize(); // 反序列DataObject對象 DataObject object = Deserialize(); // 靜態成員屬於類層級的,所以不能序列化,序列化只是序列化了對象而已, // 這裡的不能序列化的意思,是序列化資訊中不包含這個靜態成員域,下面 // 之所以i輸出還是2,是因為測試都在同一個機器(而且是同一個進程),因為這個jvm // 已經把i載入進來了,所以擷取的是載入好的i,如果是傳到另一台機器或者關掉程式重新 // 寫個程式讀入DataObject.txt,此時因為別的機器或新的進程是重新載入i的,所以i資訊就是初始時的資訊,即0 System.out.println(object); } /** * MethodName: SerializePerson * Description: 序列化Person對象 * @author * @throws FileNotFoundException * @throws IOException */ private static void Serialize() throws FileNotFoundException, IOException { DataObject object = new DataObject(); object.setWord("123"); object.setI(2); // 建立ObjectOutputStream對象輸出資料流,其中用到了檔案的描述符對象和檔案輸出資料流對象 ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream( new File("DataObject.txt"))); // 將DataObjectObject Storage Service到DataObject.txt檔案中,完成對DataObject對象的序列化操作 oo.writeObject(object); System.out.println("Person對象序列化成功。"); // 最後一定記得關閉對象描述符。。。 oo.close(); } /** * MethodName: DeserializePerson * Description: 反序列DataObject對象 * @author * @return * @throws Exception * @throws IOException */ private static DataObject Deserialize() throws Exception, IOException { // 建立ObjectInputStream對象輸入資料流,其中用到了檔案的描述符對象和檔案輸入資料流對象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream( new File("DataObject.txt"))); // 從DataObject.txt檔案中讀取DataObject對象,完成對DataObject對象的還原序列化操作 DataObject object = (DataObject) ois.readObject(); System.out.println("Person對象還原序列化成功。"); // 最後一定記得關閉對象描述符。。。 ois.close(); return object; }}
上面這段程式大家可以直接運行。注意,這裡定義了兩個方法Serialize()和Deserialize(),分別實現了序列化和還原序列化的功能,裡面的主要用到了對象輸入輸出資料流和檔案輸入輸出資料流,大家看一下程式中的注釋就可以理解。在序列化的方法中,將對象的成員變數word設定成了"123",i設定成了"2",注意這裡的i是靜態變數,那麼以通常的序列化和還原序列化的理解來看,無非就是一個正過程和一個逆過程,最終經過還原序列化後,輸出對象中的word和i時,大家一般都覺得應該還是"123"和"2",那麼上面程式的運行結果確實就是:
word = "123", i = 2
這樣會使得大家覺得理應就是如此,其實這是錯誤的。大家要記住: 靜態成員屬於類層級的,所以不能序列化,序列化只是序列化了對象而已,這裡“不能序列化”的意思是序列化資訊中不包含這個靜態成員域,下面之所以i輸出還是2,是因為測試都在同一個機器(而且是同一個進程),因為這個jvm已經把i載入進來了,所以擷取的是載入好的i,如果是傳到另一台機器或者關掉程式重新寫個程式讀入DataObject.txt,此時因為別的機器或新的進程是重新載入i的,所以i資訊就是初始時的資訊,即0。所以,總結來看,靜態成員是不能被序列化的,靜態成員定以後的預設初始值是0,所以正確的運行結果應該是:
word = "123", i = 0
那麼既然如此,怎樣才能測試出正確的結果呢。大家注意,上面的程式是直接在一個JVM一個進程中操作完了序列化和還原序列化的所有過程,故而JVM中已經儲存了i = 2,所以i的值沒有變化,所以再次讀出來肯定還是2。如果想得出正確的結果,必須在兩個JVM中去測試,但是大家的電腦很難做到這種測試環境,所以可以通過以下方法來測試。
package test2;import java.io.File;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;/** * Description: 測試對象的序列化 */public class SerializeDataobject { public static void main(String[] args) throws Exception { // 序列化DataObject對象 Serialize(); } /** * MethodName: SerializePerson * Description: 序列化Person對象 * @author * @throws FileNotFoundException * @throws IOException */ private static void Serialize() throws FileNotFoundException, IOException { DataObject object = new DataObject(); object.setWord("123"); object.setI(2); // 建立ObjectOutputStream對象輸出資料流,其中用到了檔案的描述符對象和檔案輸出資料流對象 ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream( new File("DataObject.txt"))); // 將DataObjectObject Storage Service到DataObject.txt檔案中,完成對DataObject對象的序列化操作 oo.writeObject(object); System.out.println("Person對象序列化成功。"); // 最後一定記得關閉對象描述符。。。 oo.close(); }}
上面這個類只用來進行序列化,對象被序列化後儲存在檔案"DataObject.txt"中,然後程式運行結束,JVM退出。接下來看另一段程式。
package test2;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.ObjectInputStream;/** * Description: 測試對象的反序列 */public class DeserializeDataobject { public static void main(String[] args) throws Exception { // 反序列DataObject對象 DataObject object = Deserialize(); // 靜態成員屬於類層級的,所以不能序列化,序列化只是序列化了對象而已, // 這裡的不能序列化的意思,是序列化資訊中不包含這個靜態成員域,下面 // 之所以i輸出還是2,是因為測試都在同一個機器(而且是同一個進程),因為這個jvm // 已經把i載入進來了,所以擷取的是載入好的i,如果是傳到另一台機器或者關掉程式重新 // 寫個程式讀入DataObject.txt,此時因為別的機器或新的進程是重新載入i的,所以i資訊就是初始時的資訊,即0 System.out.println(object); } /** * MethodName: DeserializePerson * Description: 反序列DataObject對象 * @author * @return * @throws Exception * @throws IOException */ private static DataObject Deserialize() throws Exception, IOException { // 建立ObjectInputStream對象輸入資料流,其中用到了檔案的描述符對象和檔案輸入資料流對象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream( new File("DataObject.txt"))); // 從DataObject.txt檔案中讀取DataObject對象,完成對DataObject對象的還原序列化操作 DataObject object = (DataObject) ois.readObject(); System.out.println("Person對象還原序列化成功。"); // 最後一定記得關閉對象描述符。。。 ois.close(); return object; }}
上面這段程式用來實現對象的還原序列化,它從檔案"DataObject.txt"中讀出對象的相關資訊,然後進行了還原序列化,最終輸出對象中word和i的值,這個程式輸出的結果才是word = "123", i = 0 這個才是正確的結果,這是因為序列化和還原序列化都有自己的main方法,先序列化,然後JVM退出,再次運行還原序列化,JVM重新載入DataObject類,此時i = 0,"DataObject.txt"檔案中其實是沒有i的資訊的,只有word的資訊。這裡通過先後執行序列化和還原序列化,讓JVM得到一次重新載入類的機會,類比了兩個JVM下啟動並執行結果。
總之,大家要記住以下幾點:
(1)序列化和還原序列化的實現方法和應用場合; (2)靜態成員是不能被序列化的,因為靜態成員是隨著類的載入而載入的,與類共存亡,並且靜態成員的預設初始值都是0; (3)要明白錯誤的那個測試程式的原因,搞明白JVM的一些基本機制; (4)要想直接通過列印對象而輸出對象的一些屬性資訊,要重寫toString方法。
上面只是我的一些個人總結,歡迎大家指正和補充。