有些時候,開發者想把程序運行過程當中的數據臨時保存到文件,但是前面介紹的字符流和字節流,要麼用來讀寫文本字符串,要麼用來讀寫字節數組,並不能直接保存某個對象信息,由於對象裏面包括成員屬性和成員方法,單就屬性而言,每一個屬性又有各自的數據類型及其具體數值,這些複雜的信息既不能經過字符串表達,也不能經過簡單的字節數組表達。雖然現有手段不容易往文件中寫入對象信息,可是該想法無疑極具吸引力,假若可以自如地對文件讀寫某個對象數據,一定會給程序員的開發工做帶來巨大便利,何況內存都能存放對象信息,爲什麼磁盤反而沒法存儲對象了呢?
解決問題的關鍵在於須要給對象創建某種映射關係,磁盤文件當然只能存放字節形式的數據,但若是能將某對象進行有規則的排序操做,使之變成整齊有序的信息隊列,那麼程序便可按照規矩把對象轉爲可存儲的字節數據。正所謂英雄所見略同,Java確實提供了相似的解題思路,把對象轉成磁盤文件可識別數據的過程,Java稱之爲「序列化」;反過來,把磁盤文件內容轉成內存中對象的過程,Java稱之爲「反序列化」。如同字符串與字節數組的相互轉換那般,序列化與反序列化一塊兒完成了內存對象和磁盤文件之間的轉換操做。
若想讓一個對象支持序列化與反序列化,得事先聲明該對象的來源類是可序列化的,也就是命令來源類實現Serializable接口,這樣程序才知道由該類建立而來的全部對象都支持序列化與反序列化。舉個用戶信息類的例子,基本的用戶信息一般包括用戶名、手機號和密碼三個字段,再添加Serializable接口的實現,因而可序列化的用戶信息類代碼變成如下這般:html
//定義一個可序列化的用戶信息類。實現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往文件寫入序列化對象的代碼例子:java
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修飾以後,從新運行對象數據讀寫代碼,根據下列的日誌信息可知密碼值已經屏蔽了序列化:orm
對象序列化成功 對象反序列化成功 用戶信息以下:姓名=王五,手機號=15960238696,密碼=null
另外,UserInfo類後續可能會增長新的成員屬性,好比整型的年齡字段。然而一旦在UserInfo的代碼定義中增長了新字段,再去讀取原先保存在文件中的序列化對象,程序運行時居然扔出異常,提示「java.io.InvalidClassException: com.io.bio.UserInfo; local class incompatible: stream classdesc serialVersionUID = ***, local class serialVersionUID = ***」,意思是本地類不兼容,IO流中的序列化編碼與本地類的序列化編碼不一致。其中的原因說來話長,對象的每次序列化都須要一個編碼serialVersionUID,程序經過該編碼來校驗讀到的對象是否爲原先的對象類型,而默認的編碼數值是根據類名、接口名、成員方法及成員屬性等聯合運算獲得的哈希值,因此只要類名、接口名、方法與屬性任何一項發生變動,都會致使serialVersionUID編碼產生變化,進而影響正常的序列化和反序列化操做。htm
這個序列化編碼的校驗規則,像極了Java版本的刻舟求劍,每次序列化的小船出發以前,都要在落劍的船身處作個標記,表示剛纔寶劍是在該位置掉進水裏的。其後小船的狀態發生了改變,譬如開到了河對岸,此時船員開始活動筋骨,準備在標記處跳下船,意圖潛水尋回寶劍。結果固然是徒勞無功,根本找不到先前落水的寶劍,由於標記刻在船身上,它跟隨着小船運動,水裏的劍未動而船已動,按照移動後的標記去找留在原地的寶劍,天然是竹籃打水一場空了。正確的作法是記下固定不動的方位信息,例如詳細的經緯度,這樣不管船怎麼開,落劍的位置都是不變的。如此一來,還需在UserInfo的定義代碼中添加如下的serialVersionUID賦值語句,從一開始就設置固定的版本編碼數值:對象
// 該類的實例在序列化時的版本編碼 private static final long serialVersionUID = 1L;
總結一下,支持序列化的類定義與普通的類定義主要有下述三項區別:
一、可序列化的類實現了Serializable接口;
二、可序列化的類須要給serialVersionUID字段賦值,避免出現版本編碼不一致的狀況;
三、可序列化的類可能有部分字段被關鍵字transient所修飾,表示這些字段無需進行序列化;
最後整合上述的三點要求,從新修改用戶信息的類定義,改後的UserInfo代碼片斷示例以下:blog
//定義一個可序列化的用戶信息類。實現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開發筆記(序)章節目錄》