摘要:這篇文章主要給大家介紹了關於java中對象的序列化與反序列化的相關內容,文中通過詳細示例代碼介紹,希望能對大家有所幫助。
本文分享自華為雲社區《java中什么是序列化和反序列化?》,原文作者:dayu_dls 。
這篇文章主要給大家介紹了關於java中對象的序列化與反序列化的相關內容,文中通過詳細示例代碼介紹,希望能對大家有所幫助。
1、序列化是干啥用的?
序列化的原本意圖是希望對一個Java對象作一下“變換”,變成字節序列,這樣一來方便持久化存儲到磁盤,避免程序運行結束后對象就從內存里消失,另外變換成字節序列也更便於網絡運輸和傳播,所以概念上很好理解:
- 序列化:把Java對象轉換為字節序列。
- 反序列化:把字節序列恢復為原先的Java對象。
而且序列化機制從某種意義上來說也彌補了平台化的一些差異,畢竟轉換后的字節流可以在其他平台上進行反序列化來恢復對象。
2、對象序列化的方式?
在Java中,如果一個對象要想實現序列化,必須要實現下面兩個接口之一:
- Serializable 接口
- Externalizable 接口
那這兩個接口是如何工作的呢?兩者又有什么關系呢?我們分別進行介紹。
2.1 Serializable 接口
一個對象想要被序列化,那么它的類就要實現此接口或者它的子接口。
這個對象的所有屬性(包括private屬性、包括其引用的對象)都可以被序列化和反序列化來保存、傳遞。不想序列化的字段可以使用transient修飾。
由於Serializable對象完全以它存儲的二進制位為基礎來構造,因此並不會調用任何構造函數,因此Serializable類無需默認構造函數,但是當Serializable類的父類沒有實現Serializable接口時,反序列化過程會調用父類的默認構造函數,因此該父類必需有默認構造函數,否則會拋異常。
使用transient關鍵字阻止序列化雖然簡單方便,但被它修飾的屬性被完全隔離在序列化機制之外,導致了在反序列化時無法獲取該屬性的值,而通過在需要序列化的對象的Java類里加入writeObject()方法與readObject()方法可以控制如何序列化各屬性,甚至完全不序列化某些屬性或者加密序列化某些屬性。
2.2 Externalizable 接口
它是Serializable接口的子類,用戶要實現的writeExternal()和readExternal() 方法,用來決定如何序列化和反序列化。
因為序列化和反序列化方法需要自己實現,因此可以指定序列化哪些屬性,而transient在這里無效。
對Externalizable對象反序列化時,會先調用類的無參構造方法,這是有別於默認反序列方式的。如果把類的不帶參數的構造方法刪除,或者把該構造方法的訪問權限設置為private、默認或protected級別,會拋出java.io.InvalidException: no valid constructor異常,因此Externalizable對象必須有默認構造函數,而且必需是public的。
2.3 對比
使用時,你只想隱藏一個屬性,比如用戶對象user的密碼pwd,如果使用Externalizable,並除了pwd之外的每個屬性都寫在writeExternal()方法里,這樣顯得麻煩,可以使用Serializable接口,並在要隱藏的屬性pwd前面加上transient就可以實現了。如果要定義很多的特殊處理,就可以使用Externalizable。
當然這里我們有一些疑惑,Serializable 中的writeObject()方法與readObject()方法科可以實現自定義序列化,而Externalizable 中的writeExternal()和readExternal() 方法也可以,他們有什么異同呢?
- readExternal(),writeExternal()兩個方法,這兩個方法除了方法簽名和readObject(),writeObject()兩個方法的方法簽名不同之外,其方法體完全一樣。
- 需要指出的是,當使用Externalizable機制反序列化該對象時,程序會使用public的無參構造器創建實例,然后才執行readExternal()方法進行反序列化,因此實現Externalizable的序列化類必須提供public的無參構造。
- 雖然實現Externalizable接口能帶來一定的性能提升,但由於實現ExternaLizable接口導致了編程復雜度的增加,所以大部分時候都是采用實現Serializable接口方式來實現序列化。
3、Serializable 如何序列化對象?
3.1 Serializable演示
然而Java目前並沒有一個關鍵字可以直接去定義一個所謂的“可持久化”對象。
對象的持久化和反持久化需要靠程序員在代碼里手動顯式地進行序列化和反序列化還原的動作。
舉個例子,假如我們要對Student類對象序列化到一個名為student.txt的文本文件中,然后再通過文本文件反序列化成Student類對象:
1、Student類定義
public class Student implements Serializable { private String name; private Integer age; private Integer score; @Override public String toString() { return "Student:" + '\n' + "name = " + this.name + '\n' + "age = " + this.age + '\n' + "score = " + this.score + '\n' ; } // ... 其他省略 ... }
2、序列化
public static void serialize( ) throws IOException { Student student = new Student(); student.setName("CodeSheep"); student.setAge( 18 ); student.setScore( 1000 ); ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) ); objectOutputStream.writeObject( student ); objectOutputStream.close(); System.out.println("序列化成功!已經生成student.txt文件"); System.out.println("=============================================="); }
3、反序列化
public static void deserialize( ) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream( new File("student.txt") ) ); Student student = (Student) objectInputStream.readObject(); objectInputStream.close(); System.out.println("反序列化結果為:"); System.out.println( student ); }
4、運行結果
控制台打印:
序列化成功!已經生成student.txt文件 ============================================== 反序列化結果為: Student: name = CodeSheep age = 18 score = 1000
3.2 Serializable接口有何用?
上面在定義Student類時,實現了一個Serializable接口,然而當我們點進Serializable接口內部查看,發現它竟然是一個空接口,並沒有包含任何方法!
試想,如果上面在定義Student類時忘了加implements Serializable時會發生什么呢?
實驗結果是:此時的程序運行會報錯,並拋出NotSerializableException異常:
我們按照錯誤提示,由源碼一直跟到ObjectOutputStream的writeObject0()方法底層一看,才恍然大悟:
如果一個對象既不是字符串、數組、枚舉,而且也沒有實現Serializable接口的話,在序列化時就會拋出NotSerializableException異常!
原來Serializable接口也僅僅只是做一個標記用!!!它告訴代碼只要是實現了Serializable接口的類都是可以被序列化的!然而真正的序列化動作不需要靠它完成。
3.3 serialVersionUID號有何用?
相信你一定經常看到有些類中定義了如下代碼行,即定義了一個名為serialVersionUID的字段:
private static final long serialVersionUID = -4392658638228508589L;
你知道這句聲明的含義嗎?為什么要搞一個名為serialVersionUID的序列號?
繼續來做一個簡單實驗,還拿上面的Student類為例,我們並沒有人為在里面顯式地聲明一個serialVersionUID字段。
我們首先還是調用上面的serialize()方法,將一個Student對象序列化到本地磁盤上的student.txt文件:
接下來我們在Student類里面動點手腳,比如在里面再增加一個名為id的字段,表示學生學號:
public class Student implements Serializable { private String name; private Integer age; private Integer score; private Integer id;
這時候,我們拿剛才已經序列化到本地的student.txt文件,還用如下代碼進行反序列化,試圖還原出剛才那個Student對象:
運行發現報錯了,並且拋出了InvalidClassException異常
這地方提示的信息非常明確了:序列化前后的serialVersionUID號碼不兼容!
從這地方最起碼可以得出兩個重要信息:
1、serialVersionUID是序列化前后的唯一標識符
2、默認如果沒有人為顯式定義過serialVersionUID,那編譯器會為它自動聲明一個!
第1個問題: serialVersionUID序列化ID,可以看成是序列化和反序列化過程中的“暗號”,在反序列化時,JVM會把字節流中的序列號ID和被序列化類中的序列號ID做比對,只有兩者一致,才能重新反序列化,否則就會報異常來終止反序列化的過程。
第2個問題: 如果在定義一個可序列化的類時,沒有人為顯式地給它定義一個serialVersionUID的話,則Java運行時環境會根據該類的各方面信息自動地為它生成一個默認的serialVersionUID,一旦像上面一樣更改了類的結構或者信息,則類的serialVersionUID也會跟着變化!
所以,為了serialVersionUID的確定性,寫代碼時還是建議,凡是implements Serializable的類,都最好人為顯式地為它聲明一個serialVersionUID明確值!
當然,如果不想手動賦值,你也可以借助IDE的自動添加功能,比如我使用的IntelliJ IDEA,按alt + enter就可以為類自動生成和添加serialVersionUID字段,十分方便:
兩種特殊情況
1、凡是被static修飾的字段是不會被序列化的
2、凡是被transient修飾符修飾的字段也是不會被序列化的
對於第一點,因為序列化保存的是對象的狀態而非類的狀態,所以會忽略static靜態域也是理所應當的。
對於第二點,就需要了解一下transient修飾符的作用了。
如果在序列化某個類的對象時,就是不希望某個字段被序列化(比如這個字段存放的是隱私值,如:密碼等),那這時就可以用transient修飾符來修飾該字段。
比如在之前定義的Student類中,加入一個密碼字段,但是不希望序列化到txt文本,則可以:
public class Student implements Serializable { private static final long serialVersionUID = -4392658638228508589L; private transient String name; private Integer age; private Integer score; private transient String passwd;
這樣在序列化Student類對象時,password字段會設置為默認值null,這一點可以從反序列化所得到的結果來看出:
public static void serialize() throws IOException { Student student = new Student(); student.setName("CodeSheep"); student.setAge(18); student.setScore(1000); student.setPasswd("123");
4、實現Externalizable
public UserInfo() { userAge=20;//這個是在第二次測試使用,判斷反序列化是否通過構造器 } public void writeExternal(ObjectOutput out) throws IOException { // 指定序列化時候寫入的屬性。這里仍然不寫入年齡 out.writeObject(userName); out.writeObject(usePass); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // 指定反序列化的時候讀取屬性的順序以及讀取的屬性 // 如果你寫反了屬性讀取的順序,你可以發現反序列化的讀取的對象的指定的屬性值也會與你寫的讀取方式一一對應。因為在文件中裝載對象是有序的 userName=(String) in.readObject(); usePass=(String) in.readObject(); }
我們在序列化對象的時候,由於這個類實現了Externalizable 接口,在writeExternal()方法里定義了哪些屬性可以序列化,哪些不可以序列化,所以,對象在經過這里就把規定能被序列化的序列化保存文件,不能序列化的不處理,然后在反序列的時候自動調用readExternal()方法,根據序列順序挨個讀取進行反序列,並自動封裝成對象返回,然后在測試類接收,就完成了反序列。
Externalizable 實例類的唯一特性是可以被寫入序列化流中,該類負責保存和恢復實例內容。 若某個要完全控制某一對象及其超類型的流格式和內容,則它要實現 Externalizable 接口的 writeExternal 和 readExternal 方法。這些方法必須顯式與超類型進行協調以保存其狀態。這些方法將代替定制的 writeObject 和 readObject 方法實現。
- writeExternal(ObjectOutput out)
該對象可實現 writeExternal 方法來保存其內容,它可以通過調用 DataOutput 的方法來保存其基本值,或調用 ObjectOutput 的 writeObject 方法來保存對象、字符串和數組。 - readExternal(ObjectInput in)
對象實現 readExternal 方法來恢復其內容,它通過調用 DataInput 的方法來恢復其基礎類型,調用 readObject 來恢復對象、字符串和數組。
externalizable和Serializable的區別:
1、實現serializable接口是默認序列化所有屬性,如果有不需要序列化的屬性使用transient修飾。externalizable接口是serializable的子類,實現這個接口需要重寫writeExternal和readExternal方法,指定對象序列化的屬性和從序列化文件中讀取對象屬性的行為。
2、實現serializable接口的對象序列化文件進行反序列化不走構造方法,載入的是該類對象的一個持久化狀態,再將這個狀態賦值給該類的另一個變量。實現externalizable接口的對象序列化文件進行反序列化先走構造方法得到控對象,然后調用readExternal方法讀取序列化文件中的內容給對應的屬性賦值。
5、序列化的受控和加強
5.1 約束性加持
從上面的過程可以看出,序列化和反序列化的過程其實是有漏洞的,因為從序列化到反序列化是有中間過程的,如果被別人拿到了中間字節流,然后加以偽造或者篡改,那反序列化出來的對象就會有一定風險了。
畢竟反序列化也相當於一種 “隱式的”對象構造 ,因此我們希望在反序列化時,進行受控的對象反序列化動作。
那怎么個受控法呢?
答案就是: 自行編寫readObject()函數,用於對象的反序列化構造,從而提供約束性。
既然自行編寫readObject()函數,那就可以做很多可控的事情:比如各種判斷工作。
還以上面的Student類為例,一般來說學生的成績應該在0 ~ 100之間,我們為了防止學生的考試成績在反序列化時被別人篡改成一個奇葩值,我們可以自行編寫readObject()函數用於反序列化的控制:
private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException { // 調用默認的反序列化函數 objectInputStream.defaultReadObject(); // 手工檢查反序列化后學生成績的有效性,若發現有問題,即終止操作! if( 0 > score || 100 < score ) { throw new IllegalArgumentException("學生分數只能在0到100之間!"); } }
比如我故意將學生的分數改為101,此時反序列化立馬終止並且報錯:
對於上面的代碼,為什么自定義的private的readObject()方法可以被自動調用,跟一下底層源碼來一探究竟,跟到了ObjectStreamClass類的最底層,是反射機制在起作用!是的,在Java里,果然萬物皆可“反射”(滑稽),即使是類中定義的private私有方法,也能被摳出來執行了,簡直引起舒適了。
5.2 單例模式增強
一個容易被忽略的問題是:可序列化的單例類有可能並不單例!
舉個代碼小例子就清楚了。
比如這里我們先用java寫一個常見的「靜態內部類」方式的單例模式實現:
public class Singleton implements Serializable { private static final long serialVersionUID = -1576643344804979563L; private Singleton() { } private static class SingletonHolder { private static final Singleton singleton = new Singleton(); } public static synchronized Singleton getSingleton() { return SingletonHolder.singleton; } }
然后寫一個驗證主函數:
public class Test2 { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream( new File("singleton.txt") ) ); // 將單例對象先序列化到文本文件singleton.txt中 objectOutputStream.writeObject( Singleton.getSingleton() ); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream( new File("singleton.txt") ) ); // 將文本文件singleton.txt中的對象反序列化為singleton1 Singleton singleton1 = (Singleton) objectInputStream.readObject(); objectInputStream.close(); Singleton singleton2 = Singleton.getSingleton(); // 運行結果竟打印 false ! System.out.println( singleton1 == singleton2 ); } }
運行后我們發現:反序列化后的單例對象和原單例對象並不相等了,這無疑沒有達到我們的目標。
解決辦法是:在單例類中手寫readResolve()函數,直接返回單例對象:
private Object readResolve() { return SingletonHolder.singleton; } package serialize.test; import java.io.Serializable; public class Singleton implements Serializable { private static final long serialVersionUID = -1576643344804979563L; private Singleton() { } private static class SingletonHolder { private static final Singleton singleton = new Singleton(); } public static synchronized Singleton getSingleton() { return SingletonHolder.singleton; } private Object readResolve() { return SingletonHolder.singleton; } }
這樣一來,當反序列化從流中讀取對象時,readResolve()會被調用,用其中返回的對象替代反序列化新建的對象。