引言
將 Java 對象序列化為二進制文件的 Java 序列化技術是 Java 系列技術中一個較為重要的技術點,在大部分情況下,開發人員只需要了解被序列化的類需要實現 Serializable 接口,使用 ObjectInputStream 和 ObjectOutputStream 進行對象的讀寫。然而在有些情況下,光知道這些還遠遠不夠,文章列舉了筆者遇到的一些真實情境,它們與 Java 序列化相關,通過分析情境出現的原因,使讀者輕松牢記 Java 序列化中的一些高級認識。
文章結構
本文將逐一的介紹幾個情境,順序如下面的列表。
- 序列化 ID 的問題
- 靜態變量序列化
- 父類的序列化與 Transient 關鍵字
- 對敏感字段加密
- 序列化存儲規則
列表的每一部分講述了一個單獨的情境,讀者可以分別查看。
序列化 ID 問題
情境:兩個客戶端 A 和 B 試圖通過網絡傳遞對象數據,A 端將對象 C 序列化為二進制數據再傳給 B,B 反序列化得到 C。
問題:C 對象的全類路徑假設為 com.inout.Test,在 A 和 B 端都有這么一個類文件,功能代碼完全一致。也都實現了 Serializable 接口,但是反序列化時總是提示不成功。
解決:虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清單 1 中,雖然兩個類的功能代碼完全一致,但是序列化 ID 不同,他們無法相互序列化和反序列化。
簡單來說,Java的序列化機制是通過在運行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常。
當實現java.io.Serializable接口的實體(類)沒有顯式地定義一個名為serialVersionUID,類型為long的變量時,Java序列化機制會根據編譯的class自動生成一個serialVersionUID作序列化版本比較用,這種情況下,只有同一次編譯生成的class才會生成相同的serialVersionUID 。
如果我們不希望通過編譯來強制划分軟件版本,即實現序列化接口的實體能夠兼容先前版本,未作更改的類,就需要顯式地定義一個名為serialVersionUID,類型為long的變量,不修改這個變量值的序列化實體都可以相互進行串行化和反串行化。
清單 1. 相同功能代碼不同序列化 ID 的類對比
package com.inout; import java.io.Serializable; public class A implements Serializable { private static final long serialVersionUID = 1L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } package com.inout; import java.io.Serializable; public class A implements Serializable { private static final long serialVersionUID = 2L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
序列化 ID 在 Eclipse 下提供了兩種生成策略,一個是固定的 1L,一個是隨機生成一個不重復的 long 類型數據(實際上是使用 JDK 工具生成),在這里有一個建議,如果沒有特殊需求,就是用默認的 1L 就可以,這樣可以確保代碼一致時反序列化成功。那么隨機生成的序列化 ID 有什么作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。
特性使用案例
讀者應該聽過 Façade 模式,它是為應用程序提供統一的訪問接口,案例程序中的 Client 客戶端使用了該模式,案例程序結構圖如圖 1 所示。
圖 1. 案例程序結構

Client 端通過 Façade Object 才可以與業務邏輯對象進行交互。而客戶端的 Façade Object 不能直接由 Client 生成,而是需要 Server 端生成,然后序列化后通過網絡將二進制對象數據傳給 Client,Client 負責反序列化得到 Façade 對象。該模式可以使得 Client 端程序的使用需要服務器端的許可,同時 Client 端和服務器端的 Façade Object 類需要保持一致。當服務器端想要進行版本更新時,只要將服務器端的 Façade Object 類的序列化 ID 再次生成,當 Client 端反序列化 Façade Object 就會失敗,也就是強制 Client 端從服務器端獲取最新程序。
靜態變量序列化
情境:查看清單 2 的代碼。
清單 2. 靜態變量序列化問題代碼
package com.inout; import java.io.Serializable; public class StaticMemberVarriableSerializable implements Serializable { private static final long serialVersionUID = 1L; public static int staticVar = 15; } package com.inout; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class StaticMemberVarriableSerializableWrite { public static void main(String[] args) { try { // 初始時staticVar為5 File file = new File("D:" + File.separator + "ss.txt"); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); out.writeObject(new StaticMemberVarriableSerializable()); out.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
先將StaticMemberVarriableSerializable類序列化到d:\ss.txt文件中。
接着讀上面的文件進行反序列化啦:
package com.inout; import java.io.Serializable; public class StaticMemberVarriableSerializable implements Serializable { private static final long serialVersionUID = 1L; public static int staticVar = 15; } package com.inout; 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; import java.io.Serializable; public class StaticMemberVarriableSerializableRead { public static void main(String[] args) { try { File file = new File("D:" + File.separator + "ss.txt"); ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); StaticMemberVarriableSerializable t = (StaticMemberVarriableSerializable) oin.readObject(); oin.close(); // 再讀取,通過t.staticVar打印新的值 System.out.println("staticVar:" + t.staticVar); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
結果:
15
清單 2 中的 main 方法,將對象序列化后,修改靜態變量的數值,再將序列化對象讀取出來,然后通過讀取出來的對象獲得靜態變量的數值並打印出來。依照清單 2,這個 System.out.println(t.staticVar) 語句輸出的是 15 還是 5 呢?
最后的輸出是 15,對於無法理解的讀者認為,打印的 staticVar 是從讀取的對象里獲得的,應該是保存時的狀態才對。之所以打印 10 的原因在於序列化時,並不保存靜態變量,這其實比較容易理解,序列化保存的是對象的狀態,靜態變量屬於類的狀態,因此 序列化並不保存靜態變量。
父類的序列化與 Transient 關鍵字
情境:一個子類實現了 Serializable 接口,它的父類都沒有實現 Serializable 接口,序列化該子類對象,然后反序列化后輸出父類定義的某變量的數值,該變量數值與序列化時的數值不同。
解決:要想將父類對象也序列化,就需要讓父類也實現Serializable 接口。如果父類不實現的話的,就需要有默認的無參的構造函數。在父類沒有實現 Serializable 接口時,虛擬機是不會序列化父對象的,而一個 Java 對象的構造必須先有父對象,才有子對象,反序列化也不例外。所以反序列化時,為了構造父對象,只能調用父類的無參構造函數作為默認的父對象。因此當我們取父對象的變量值時,它的值是調用父類無參構造函數后的值。如果你考慮到這種序列化的情況,在父類無參構造函數中對變量進行初始化,否則的話,父類變量值都是默認聲明的值,如 int 型的默認是 0,string 型的默認是 null。
Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設為初始值,如 int 型的是 0,對象型的是 null。
特性使用案例
我們熟悉使用 Transient 關鍵字可以使得字段不被序列化,那么還有別的方法嗎?根據父類對象序列化的規則,我們可以將不需要被序列化的字段抽取出來放到父類中,子類實現 Serializable 接口,父類不實現,根據父類序列化規則,父類的字段數據將不被序列化,形成類圖如圖 2 所示。
圖 2. 案例程序類圖

上圖中可以看出,attr1、attr2、attr3、attr5 都不會被序列化,放在父類中的好處在於當有另外一個 Child 類時,attr1、attr2、attr3 依然不會被序列化,不用重復抒寫 transient,代碼簡潔。
代碼清單:
package com.inout; public class Parent { private String attr1; private String attr2; private String attr3; public Parent() { } public Parent(String attr1, String attr2, String attr3) { this.attr1 = attr1; this.attr2 = attr2; this.attr3 = attr3; } public String getAttr1() { return attr1; } public void setAttr1(String attr1) { this.attr1 = attr1; } public String getAttr2() { return attr2; } public void setAttr2(String attr2) { this.attr2 = attr2; } public String getAttr3() { return attr3; } public void setAttr3(String attr3) { this.attr3 = attr3; } }
package com.inout; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; public class Child extends Parent implements Serializable { private String attr4; private transient String attr5; public String getAttr4() { return attr4; } public void setAttr4(String attr4) { this.attr4 = attr4; } public String getAttr5() { return attr5; } public void setAttr5(String attr5) { this.attr5 = attr5; } public Child(String attr1, String attr2, String attr3, String attr4, String attr5) { super(attr1, attr2, attr3); this.attr4 = attr4; this.attr5 = attr5; } public static void main(String[] args) throws IOException, ClassNotFoundException { File file = new File("D:" + File.separator + "s.txt"); OutputStream os = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(new Child("str1", "str2","str3", "str4","str5")); oos.close(); InputStream is = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(is); Child so = (Child) ois.readObject(); System.out.println("str1 = " + so.getAttr1()); System.out.println("str2 = " + so.getAttr2()); System.out.println("str3 = " + so.getAttr3()); System.out.println("str4 = " + so.getAttr4()); System.out.println("str5 = " + so.getAttr5()); ois.close(); } }
結果:
str1 = null str2 = null str3 = null str4 = str4 str5 = null
對敏感字段加密
情境:服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,才可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。
解決:在序列化過程中,虛擬機會試圖調用對象類里的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。基於這個原理,可以在實際應用中得到使用,用於敏感字段的加密工作,清單 3 展示了這個過程。
代碼清單:
package com.inout; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectInputStream.GetField; import java.io.ObjectOutputStream; import java.io.ObjectOutputStream.PutField; import java.io.OutputStream; import java.io.Serializable; public class SensitiveEncryption implements Serializable { private static final long serialVersionUID = 1L; private String loginName; //private transient String password; private String password; public SensitiveEncryption(String loginName, String password) { this.loginName = loginName; this.password = password; } private void writeObject(ObjectOutputStream out) { try { PutField putFields = out.putFields(); putFields.put("loginName", loginName); System.out.println("原密碼:" + password); password = "encryption";//模擬加密 putFields.put("password", password); System.out.println("加密后的密碼" + password); out.writeFields(); } catch (IOException e) { e.printStackTrace(); } } private void readObject(ObjectInputStream in) { try { GetField readFields = in.readFields(); Object object1 = readFields.get("loginName", ""); Object object2 = readFields.get("password", ""); System.out.println("要解密的字符串:" + object2.toString()); //password = "pass";//模擬解密,需要獲得本地的密鑰 password = object2.toString(); loginName = object1.toString(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public String getLoginName() { return loginName; } public void setLoginName(String loginName) { this.loginName = loginName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public static void main(String[] args) throws IOException, ClassNotFoundException { File file = new File("D:" + File.separator + "sss.txt"); OutputStream os = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(new SensitiveEncryption("str1", "str2")); oos.close(); InputStream is = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(is); SensitiveEncryption so = (SensitiveEncryption) ois.readObject(); System.out.println("loginName = " + so.getLoginName()); System.out.println("password = " + so.getPassword()); ois.close(); } }
執行結果:
原密碼:str2 加密后的密碼encryption 要解密的字符串:encryption loginName = str1 password = encryption
在清單 的 writeObject 方法中,對密碼進行了加密,在 readObject 中則對 password 進行解密,只有擁有密鑰的客戶端,才可以正確的解析出密碼,確保了數據的安全。
特性使用案例
RMI 技術是完全基於 Java 序列化技術的,服務器端接口調用所需要的參數對象來至於客戶端,它們通過網絡相互傳輸。這就涉及 RMI 的安全傳輸的問題。一些敏感的字段,如用戶名密碼(用戶登錄時需要對密碼進行傳輸),我們希望對其進行加密,這時,就可以采用本節介紹的方法在客戶端對密碼進行加密,服務器端進行解密,確保數據傳輸的安全性。
序列化存儲規則
情境:問題代碼如清單 4 所示。
清單 4. 存儲規則問題代碼
package com.inout; 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; import java.io.Serializable; public class SerializableStore implements Serializable { private String str1; public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj")); SerializableStore test = new SerializableStore(); // 試圖將對象兩次寫入文件 out.writeObject(test); out.flush(); System.out.println(new File("result.obj").length()); out.writeObject(test); out.close(); System.out.println(new File("result.obj").length()); ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj")); // 從文件依次讀出兩個文件 SerializableStore t1 = (SerializableStore) oin.readObject(); SerializableStore t2 = (SerializableStore) oin.readObject(); oin.close(); // 判斷兩個引用是否指向同一個對象 System.out.println(t1 == t2); } }
清單 中對同一對象兩次寫入文件,打印出寫入一次對象后的存儲大小和寫入兩次后的存儲大小,然后從文件中反序列化出兩個對象,比較這兩個對象是否為同一對象。一般的思維是,兩次寫入對象,文件大小會變為兩倍的大小,反序列化時,由於從文件讀取,生成了兩個對象,判斷相等時應該是輸入 false 才對,但是最后結果輸出如圖 4 所示。
圖 4. 示例程序輸出
77
82
true
我們看到,第二次寫入對象時文件只增加了 5 字節,並且兩個對象是相等的,這是為什么呢?
解答:Java 序列化機制為了節省磁盤空間,具有特定的存儲規則,當寫入文件的為同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用,上面增加的 5 字節的存儲空間就是新增引用和一些控制信息的空間。反序列化時,恢復引用關系,使得清單 中的 t1 和 t2 指向唯一的對象,二者相等,輸出 true。該存儲規則極大的節省了存儲空間。
特性案例分析
查看清單 5 的代碼。
清單 5. 案例代碼
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj")); Test test = new Test(); test.i = 1; out.writeObject(test); out.flush(); test.i = 2; out.writeObject(test); out.close(); ObjectInputStream oin = new ObjectInputStream(new FileInputStream( "result.obj")); Test t1 = (Test) oin.readObject(); Test t2 = (Test) oin.readObject(); System.out.println(t1.i); System.out.println(t2.i);
清單 4 的目的是希望將 test 對象兩次保存到 result.obj 文件中,寫入一次以后修改對象屬性值再次保存第二次,然后從 result.obj 中再依次讀出兩個對象,輸出這兩個對象的 i 屬性值。案例代碼的目的原本是希望一次性傳輸對象修改前后的狀態。
結果兩個輸出的都是 1, 原因就是第一次寫入對象以后,第二次再試圖寫的時候,虛擬機根據引用關系知道已經有一個相同對象已經寫入文件,因此只保存第二次寫的引用,所以讀取時,都是第一次保存的對象。讀者在使用一個文件多次 writeObject 需要特別注意這個問題。
默認序列化
序列化只需要實現java.io.Serializable接口就可以了。序列化的時候有一個serialVersionUID參數,Java序列化機制是通過在運行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化,Java虛擬機會把傳過來的字節流中的serialVersionUID和本地相應實體類的serialVersionUID進行比較,如果相同就認為是一致的實體類,可以進行反序列化,否則Java虛擬機會拒絕對這個實體類進行反序列化並拋出異常。serialVersionUID有兩種生成方式:
1、默認的1L
2、根據類名、接口名、成員方法以及屬性等來生成一個64位的Hash字段
如果實現java.io.Serializable接口的實體類沒有顯式定義一個名為serialVersionUID、類型為long的變量時,Java序列化機制會根據編譯的.class文件自動生成一個serialVersionUID,如果.class文件沒有變化,那么就算編譯再多次,serialVersionUID也不會變化。換言之,Java為用戶定義了默認的序列化、反序列化方法,其實就是ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。看一個例子:
package com.dxz.serializable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; public class SerializableObject implements Serializable { private static final long serialVersionUID = 1L; private String str0; private transient String str1; private static String str2 = "abc"; public SerializableObject(String str0, String str1) { this.str0 = str0; this.str1 = str1; } public String getStr0() { return str0; } public String getStr1() { return str1; } public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "s.txt"); OutputStream os = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(new SerializableObject("str0", "str1")); oos.close(); InputStream is = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(is); SerializableObject so = (SerializableObject) ois.readObject(); System.out.println("str0 = " + so.getStr0()); System.out.println("str1 = " + so.getStr1()); ois.close(); } }
先不運行,用一個二進制查看器查看一下s.txt這個文件,並詳細解釋一下每一部分的內容。

第1部分是序列化文件頭
◇AC ED:STREAM_MAGIC序列化協議
◇00 05:STREAM_VERSION序列化協議版本
◇73:TC_OBJECT聲明這是一個新的對象
第2部分是要序列化的類的描述,在這里是SerializableObject類
◇72:TC_CLASSDESC聲明這里開始一個新的class
◇00 1F:十進制的31,表示class名字的長度是31個字節
◇63 6F 6D ... 65 63 74:表示的是“com.xrq.test.SerializableObject”這一串字符,可以數一下確實是31個字節
◇00 00 00 00 00 00 00 01:SerialVersion,序列化ID,1
◇02:標記號,聲明該對象支持序列化
◇00 01:該類所包含的域的個數為1個
第3部分是對象中各個屬性項的描述
◇4C:字符"L",表示該屬性是一個對象類型而不是一個基本類型
◇00 04:十進制的4,表示屬性名的長度
◇73 74 72 30:字符串“str0”,屬性名
◇74:TC_STRING,代表一個new String,用String來引用對象
第4部分是該對象父類的信息,如果沒有父類就沒有這部分。有父類和第2部分差不多
◇00 12:十進制的18,表示父類的長度
◇4C 6A 61 ... 6E 67 3B:“L/java/lang/String;”表示的是父類屬性
◇78:TC_ENDBLOCKDATA,對象塊結束的標志
◇70:TC_NULL,說明沒有其他超類的標志
第5部分輸出對象的屬性項的實際值,如果屬性項是一個對象,這里還將序列化這個對象,規則和第2部分一樣
◇00 04:十進制的4,屬性的長度
◇73 74 72 30:字符串“str0”,str0的屬性值
從以上對於序列化后的二進制文件的解析,我們可以得出以下幾個關鍵的結論:
1、序列化之后保存的是類的信息
2、被聲明為transient的屬性不會被序列化,這就是transient關鍵字的作用
3、被聲明為static的屬性不會被序列化,這個問題可以這么理解,序列化保存的是對象的狀態,但是static修飾的變量是屬於類的而不是屬於變量的,因此序列化的時候不會序列化它
接下來運行一下上面的代碼看一下
str0 = str0 str1 = null
因為str1是一個transient類型的變量,沒有被序列化,因此反序列化出來也是沒有任何內容的,顯示的null,符合我們的結論。
手動指定序列化過程
Java並不強求用戶非要使用默認的序列化方式,用戶也可以按照自己的喜好自己指定自己想要的序列化方式----只要你自己能保證序列化前后能得到想要的數據就好了。手動指定序列化方式的規則是:
進行序列化、反序列化時,虛擬機會首先試圖調用對象里的writeObject和readObject方法,進行用戶自定義的序列化和反序列化。如果沒有這樣的方法,那么默認調用的是ObjectOutputStream的defaultWriteObject以及ObjectInputStream的defaultReadObject方法。換言之,利用自定義的writeObject方法和readObject方法,用戶可以自己控制序列化和反序列化的過程。
這是非常有用的。比如:
1、有些場景下,某些字段我們並不想要使用Java提供給我們的序列化方式,而是想要以自定義的方式去序列化它,比如ArrayList的 elementData、HashMap的table(至於為什么在之后寫這兩個類的時候會解釋原因),就可以通過將這些字段聲明為transient, 然后在writeObject和readObject中去使用自己想要的方式去序列化它們。
2、因為 序列化並不安全,因此有些場景下我們需要對一些敏感字段進行加密再序列化,然后再反序列化的時候按照同樣的方式進行解密,就在一定程度上保證了安全性了。 要這么做,就必須自己寫writeObject和readObject,writeObject方法在序列化前對字段加密,readObject方法在序 列化之后對字段解密。
上面的例子SerializObject這個類修改一下,主函數不需要修改
package com.dxz.serializable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; public class SerializableObject implements Serializable { private static final long serialVersionUID = 1L; private String str0; private transient String str1; private static String str2 = "abc"; public SerializableObject(String str0, String str1) { this.str0 = str0; this.str1 = str1; } public String getStr0() { return str0; } public String getStr1() { return str1; } private void writeObject(java.io.ObjectOutputStream s) throws Exception { System.out.println("我想自己控制序列化的過程"); s.defaultWriteObject(); s.writeInt(str1.length()); for (int i = 0; i < str1.length(); i++) s.writeChar(str1.charAt(i)); } private void readObject(java.io.ObjectInputStream s) throws Exception { System.out.println("我想自己控制反序列化的過程"); s.defaultReadObject(); int length = s.readInt(); char[] cs = new char[length]; for (int i = 0; i < length; i++) cs[i] = s.readChar(); str1 = new String(cs, 0, length); } public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "s.txt"); OutputStream os = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(new SerializableObject("str0", "str1")); oos.close(); InputStream is = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(is); SerializableObject so = (SerializableObject) ois.readObject(); System.out.println("str0 = " + so.getStr0()); System.out.println("str1 = " + so.getStr1()); ois.close(); } }
直接看一下運行結果
我想自己控制序列化的過程 我想自己控制反序列化的過程 str0 = str0 str1 = str1
看到,程序走到了我們自己寫的writeObject和readObject中,而且被transient修飾的str1也成功序列化、反序列化出來了----因為手動將str1寫入了文件和從文件中讀了出來。不妨再看一下s.txt文件的二進制:

看到橘黃色的部分就是writeObject方法追加的str1的內容。至此,總結一下writeObject和readObject的通常用法:
先通過defaultWriteObject和defaultReadObject方法序列化、反序列化,然后在文件結尾追加需要額外序列化的內容/從文件的結尾讀取額外需要讀取的內容。
我們自己寫的writeObject和readObject為什么會被調用?
在ObjectStreamClass.java中會通過反射查找目標對象是否有writeObject和readObject方法,如下圖:

如果有就調用,否則調用

復雜序列化情況總結
雖然Java的序列化能夠保證對象狀態的持久保存,但是遇到一些對象結構復雜的情況還是比較難處理的,最后對一些復雜的對象情況作一個總結:
1、當父類繼承Serializable接口時,所有子類都可以被序列化
2、子類實現了Serializable接口,父類沒有,父類需要有默認的構造函數,父類中的屬性不能序列化(不報錯,數據丟失),但是在子類中屬性仍能正確序列化
3、如果序列化的屬性是對象,則這個對象也必須實現Serializable接口,否則會報錯
4、反序列化時,如果對象的屬性有修改或刪減,則修改的部分屬性會丟失,但不會報錯
5、反序列化時,如果serialVersionUID被修改,則反序列化時會失敗
小結
本文通過幾個具體的情景,介紹了 Java 序列化的一些高級知識,雖說高級,並不是說讀者們都不了解,希望用筆者介紹的情景讓讀者加深印象,能夠更加合理的利用 Java 序列化技術,在未來開發之路上遇到序列化問題時,可以及時的解決。由於本人知識水平有限,文章中倘若有錯誤的地方,歡迎聯系我批評指正。
平時我們在Java內存中的對象,是無法進行IO操作或者網絡通信的,因為在進行IO操作或者網絡通信的時候,人家根本不知道內存中的對象是個什么東西,因此必須將對象以某種方式表示出來,即存儲對象中的狀態。一個Java對象的表示有各種各樣的方式,Java本身也提供給了用戶一種表示對象的方式,那就是序列化。換句話說,序列化只是表示對象的一種方式而已。OK,有了序列化,那么必然有反序列化,我們先看一下序列化、反序列化是什么意思。
序列化:將一個對象轉換成一串二進制表示的字節數組,通過保存或轉移這些字節數據來達到持久化的目的。
反序列化:將字節數組重新構造成對象。
