1. 程式人生 > >Java開發筆記(九十)對象序列化及其讀寫

Java開發筆記(九十)對象序列化及其讀寫

oca 裏的 例如 復雜 默認 輸入 之前 實現 寫文本

有些時候,開發者想把程序運行過程中的數據臨時保存到文件,可是前面介紹的字符流和字節流,要麽用來讀寫文本字符串,要麽用來讀寫字節數組,並不能直接保存某個對象信息,因為對象裏面包括成員屬性和成員方法,單就屬性而言,每個屬性又有各自的數據類型及其具體數值,這些復雜的信息既不能通過字符串表達,也不能通過簡單的字節數組表達。雖然現有手段不容易往文件中寫入對象信息,但是該想法無疑極具吸引力,倘若能夠自如地對文件讀寫某個對象數據,必定會給程序員的開發工作帶來巨大便利,況且內存都能存放對象信息,為何磁盤反而無法存儲對象了呢?
解決問題的關鍵在於需要給對象建立某種映射關系,磁盤文件固然只能存放字節形式的數據,但如果能將某對象進行有規則的排序操作,使之變成整齊有序的信息隊列,那麽程序即可按照規矩把對象轉為可存儲的字節數據。正所謂英雄所見略同,Java確實提供了類似的解題思路,把對象轉成磁盤文件可識別數據的過程,Java稱之為“序列化”;反過來,把磁盤文件內容轉成內存中對象的過程,Java稱之為“反序列化”。如同字符串與字節數組的相互轉換那般,序列化與反序列化一起完成了內存對象和磁盤文件之間的轉換操作。

若想讓一個對象支持序列化與反序列化,得事先聲明該對象的來源類是可序列化的,也就是命令來源類實現Serializable接口,這樣程序才知道由該類創建而來的所有對象都支持序列化與反序列化。舉個用戶信息類的例子,基本的用戶信息通常包括用戶名、手機號和密碼三個字段,再添加Serializable接口的實現,於是可序列化的用戶信息類代碼變成以下這般:

//定義一個可序列化的用戶信息類。實現Serializable接口表示當前類支持序列化
public class UserInfo implements Serializable {
	private String name; // 用戶名
	private String phone; // 手機號碼
	private String password; // 密碼

	public UserInfo() {
		name = "";
		phone = "";
		password = "";
	}

	// 以下省略各字段的get***/set***方法
}

之後來自於UserInfo的用戶對象們紛紛搖身變為結構清晰的實例,不過由於序列化後的對象是種特殊的數據,因此還需專門的輸入輸出流進行處理。讀寫序列化對象的專用I/O流包括對象輸入流ObjectInputStream和對象輸出流ObjectOutputStream,其中前者用來從文件中讀取對象信息,它的readObject方法完成了讀對象操作;後者用來將對象信息寫入文件,它的writeObject方法完成了寫對象操作。下面是利用ObjectOutputStream往文件寫入序列化對象的代碼例子:

	private static String mFileName = "D:/test/user.txt";
	// 利用對象輸出流把序列化對象寫入文件
	private static void writeObject() {
		// 下面創建可序列化的用戶信息對象,並給予賦值
		UserInfo user = new UserInfo();
		user.setName("王五");
		user.setPhone("15960238696");
		user.setPassword("111111");
		// 根據指定文件路徑構建文件輸出流對象,然後據此構建對象輸出流對象
		try (FileOutputStream fos = new FileOutputStream(mFileName);
				ObjectOutputStream oos = new ObjectOutputStream(fos);) {
			oos.writeObject(user); // 把對象信息寫入文件
			System.out.println("對象序列化成功");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

由此可見,將對象信息寫入文件的代碼還是蠻簡單的,從文件讀取對象信息也很容易,只要下面的寥寥幾行代碼就搞定了:

	// 利用對象輸入流從文件中讀取序列化對象
	private static void readObject() {
		// 創建可序列化的用戶信息對象
		UserInfo user = new UserInfo();
		// 根據指定文件路徑構建文件輸入流對象,然後據此構建對象輸入流對象
		try (FileInputStream fos = new FileInputStream(mFileName);
				ObjectInputStream ois = new ObjectInputStream(fos);) {
			user = (UserInfo) ois.readObject(); // 從文件讀取對象信息
			System.out.println("對象反序列化成功");
		} catch (Exception e) {
			e.printStackTrace();
		}
		// 註意用戶信息的密碼字段設置了禁止序列化,故而文件讀到的密碼字段為空
		String desc = String.format("姓名=%s,手機號=%s,密碼=%s", 
				user.getName(), user.getPhone(), user.getPassword());
		System.out.println("用戶信息如下:"+desc);
	}

然後運行上述的對象數據讀寫代碼,觀察到下列的日誌信息:

對象序列化成功
對象反序列化成功
用戶信息如下:姓名=王五,手機號=15960238696,密碼=111111

看到這些日誌,有沒有發現什麽不對勁的地方?也許有人猛然驚醒,密碼這麽重要的字段居然會從文件裏讀到了明文?趕緊找到示例代碼中的磁盤文件user.txt,使用文本編輯軟件如UEStudio打開user.txt,在該文件末尾附近赫然出現了六位數字密碼111111,詳見下圖所示的右下角。

技術分享圖片

顯然密碼值不應保存在文件裏面,尤其是光天化日之下也能看到的明文。可見對象序列化應當有所取舍,尋常字段允許序列化,而私密字段不允許序列化。為此Java新增了關鍵字transient,凡是被transient修飾的字段,會在序列化之時自動予以屏蔽,也就是說,序列化無法保存該字段的數值。如此一來,用戶信息UserInfo的類定義需要把password密碼字段的聲明代碼改成下面這樣:

	// 關鍵字transient可讓它所修飾的字段無法序列化,也就是說,序列化無法保存該字段的數值
	private transient String password; // 密碼

給密碼字段添加了transient修飾之後,重新運行對象數據讀寫代碼,根據下列的日誌信息可知密碼值已經屏蔽了序列化:

對象序列化成功
對象反序列化成功
用戶信息如下:姓名=王五,手機號=15960238696,密碼=null

另外,UserInfo類後續可能會增加新的成員屬性,比如整型的年齡字段。然而一旦在UserInfo的代碼定義中增加了新字段,再去讀取原先保存在文件中的序列化對象,程序運行時竟然扔出異常,提示“java.io.InvalidClassException: com.io.bio.UserInfo; local class incompatible: stream classdesc serialVersionUID = ***, local class serialVersionUID = ***”,意思是本地類不兼容,IO流中的序列化編碼與本地類的序列化編碼不一致。其中的緣由說來話長,對象的每次序列化都需要一個編碼serialVersionUID,程序通過該編碼來校驗讀到的對象是否為原先的對象類型,而默認的編碼數值是根據類名、接口名、成員方法及成員屬性等聯合運算得到的哈希值,所以只要類名、接口名、方法與屬性任何一項發生變更,都會導致serialVersionUID編碼產生變化,進而影響正常的序列化和反序列化操作。

這個序列化編碼的校驗規則,像極了Java版本的刻舟求劍,每次序列化的小船出發之前,都要在落劍的船身處做個標記,表示剛才寶劍是在該位置掉進水裏的。其後小船的狀態發生了改變,譬如開到了河對岸,此時船員開始活動筋骨,準備在標記處跳下船,意圖潛水尋回寶劍。結果當然是徒勞無功,根本找不到先前落水的寶劍,因為標記刻在船身上,它跟隨著小船運動,水裏的劍未動而船已動,按照移動後的標記去找留在原地的寶劍,自然是竹籃打水一場空了。正確的做法是記下固定不動的方位信息,例如詳細的經緯度,這樣無論船怎麽開,落劍的位置都是不變的。如此一來,還需在UserInfo的定義代碼中添加以下的serialVersionUID賦值語句,從一開始就設置固定的版本編碼數值:

	// 該類的實例在序列化時的版本編碼
	private static final long serialVersionUID = 1L;

總結一下,支持序列化的類定義與普通的類定義主要有下述三項區別:
1、可序列化的類實現了Serializable接口;
2、可序列化的類需要給serialVersionUID字段賦值,避免出現版本編碼不一致的情況;
3、可序列化的類可能有部分字段被關鍵字transient所修飾,表示這些字段無需進行序列化;
最後整合上述的三點要求,重新修改用戶信息的類定義,改後的UserInfo代碼片段示例如下:

//定義一個可序列化的用戶信息類。實現Serializable接口表示當前類支持序列化
public class UserInfo implements Serializable {
	// 該類的實例在序列化時的版本編碼
	private static final long serialVersionUID = 1L;

	private String name; // 用戶名
	private String phone; // 手機號碼
	// 關鍵字transient可讓它所修飾的字段無法序列化,也就是說,序列化無法保存該字段的數值
	private transient String password; // 密碼

	public UserInfo() {
		name = "";
		phone = "";
		password = "";
	}

	// 以下省略各字段的get***/set***方法
}

  

更多Java技術文章參見《Java開發筆記(序)章節目錄》

Java開發筆記(九十)對象序列化及其讀寫