談談序列化和反序列化
- 2020 年 3 月 16 日
- 筆記
序列化簡介
Java序列化是指將一個Java對象轉化為一個二進位流的過程,反序列化是指將二進位流轉化為一個Java對象的過程。一般進行序列化的目的有:
- 當程式退出時, 這些對象也就消失了, 而序列化正是為了將這些對象保存起來以僅將來使用;
- 通過網路將序列化後的二進位流傳輸給遠程
JVM
使用(RPC
、RMI
的基礎)。
所有可能在網路上傳輸的對象都應該是可以序列化的,比如RMI
過程的中參數和返回值;所有需要保存到磁碟中的對象也應該是可以序列化的,比如說需要保存到HttpSession
或者ServletContext
中的對象。對象要實現序列化,必須實現以下兩個介面的其中一個:
- Serializable
- Externalizable
這邊簡單介紹這兩個介面的區別。
Externalizable繼承了Serializable,該介面中定義了兩個抽象方法:writeExternal()與readExternal()。當使用Externalizable介面來進行序列化與反序列化的時候需要開發人員重寫writeExternal()與readExternal()方法。還有一點值得注意:在使用Externalizable進行序列化的時候,在讀取對象時,會調用被序列化類的無參構造器去創建一個新的對象,然後再將被保存對象的欄位的值分別填充到新對象中。所以,實現Externalizable介面的類必須要提供一個public的無參的構造器。
一般當我們序列化和反序列化時要實現些自定義邏輯時,會實現Externalizable介面。
對象流
我們要將對象進行序列化,可以使用ObjectOutputStream
和ObjectInputStream
進行序列化。
//使用了自動關閉資源的語法糖 try( ObjectOutputStrem oos = new ObjectOutputStrem(new FileOutputStream("object.txt")) ){ Person p = new Person(); oos.writeObject(p); }catch(Exception e){ //handle Exception }
以上程式碼是將對象序列化到本地文件中,想要將對象反序列化,可以使用ObjectInputStream。使用方式和上面的程式碼類似。反序列化時僅僅讀取的是Java對象的數據,讀不到類的數據,因此反序列化時必須要提供這個對象對應的Class文件,不然會報類找不到異常。另外反序列化時不會通過類的構造函數來初始化類。
當一個可序列化的類有多個父類時(包括直接父類和間接父類),這些父類要麼有無參數的構造函數,要麼也是可以序列化的,否則這個子類進行反序列化時將拋出InvalidClassException
。如果父類是不可以序列化的,只是帶有無參數的構造函數,那麼子類在進行序列化的時候不會將父類中定義的成員變數序列化到二進位流中。
引用類型成員變數的序列化
如果一個類中包含引用變數,那麼只有這個引用變數是可序列化的,這個類本身才是可以序列化的。不然的話無論你是否實現Serializable
,都是不能進行序列化的。
所有對象只會被序列化一次,不然話會存在這樣一個問題:對象A和B屬於同一個類,他們同時引用了對象C。如果我們序列化A和B時,將對象C序列化兩次的話,那麼在我們反序列化的時候系統中會有兩個C對象。這和我們序列化的初衷不符合。因此Java在序列化的時候採用了下面的序列化演算法:
- 所有保存到磁碟的對象都會有一個序列化編號;
- 當程式試圖序列化一個對象時,程式會先檢查該對象是否已經被序列化過,只有該對象從未被序列化過(本次JVM中),才會將該對象轉換成位元組序列輸出;
- 如果該對象已經序列化過,程式只會輸出一個序列化編號,不會對該對象再次序列化。
如果是一個可變對象,我們在將其序列化之後,改變對象的內容,然後再試圖對該對象進行序列化,這樣是不會生效的。
自定義序列化
當對某個對象進行序列化時,系統會自動把該對象所有實例變數依次進行序列化,如果某個實例引用到另一個對象,則被引用的對象也會被序列化。
如果我們不希望對象的某個屬性被序列化,那麼我們可以在定義這個成員變數時加上transient
關鍵字。transient
關鍵字只能修飾實例變數。
transient
提供的機制過於簡單,如果開發者想對某個實例變數進行比較複雜的序列化機制應該怎麼做呢?在序列化和反序列化的過程中,如果對象需要特殊的處理邏輯,那麼這些對象要提供如下的方法:
//可以通過此方法修改序列化的對象 private Object writeReplace() throws ObjectStreamException; //方法中調用 private void writeObject(java.io.ObjectOutputStream out) throws IOException; //使用writeObject的默認的序列化方式,除此之外可以加上一些其他的操作,如添加額外的序列化對象到輸出:out.writeObject("XX") defaultWriteObject() private void readObject(java.io.ObjectInputStream in) throws Exception; //可以通過此方法修改返回的對象 private Object readResolve() throws ObjectStreamException;
下面給出一個單例序列化和反序列化的列子:
public class PersonSingleton implements Serializable { private static final long serialVersionUID = 1L; private String name; private PersonSingleton(String name) { this.name = name; }; private static PersonSingleton person = null; public static synchronized PersonSingleton getInstance() { if (person == null) return person = new PersonSingleton("cgl"); return person; } private Object writeReplace() throws ObjectStreamException { System.out.println("1 write replace start"); return this;//可修改為其他對象 } private void writeObject(java.io.ObjectOutputStream out) throws IOException { System.out.println("2 write object start"); out.defaultWriteObject(); //out.writeInt(1); } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { System.out.println("3 read object start"); in.defaultReadObject(); //int i=in.readInt(); } private Object readResolve() throws ObjectStreamException { System.out.println("4 read resolve start"); return PersonSingleton.getInstance();//不管序列化的操作是什麼,返回的都是本地的單例對象 } }
另外Java中還提供了另外一種自定義序列化的機制,就是實現Externalizable
介面,這種機製程式設計師在序列化過程中能有更大的控制權。
serialVersionUID 作用
JVM如何判斷序列化與反序列化的類文件是否相同呢?
並不是說兩個類文件要完全一樣, 而是通過類的一個私有屬性serialVersionUID來判斷的, 如果我們沒有顯示的指定這個屬性, 那麼JVM會自動使用該類的hashcode值來設置這個屬性, 這個時候如果我們對類進行改變(比如說加一個屬性或者刪掉一個屬性)就會導致serialVersionUID不同, 所以對於準備序列化的類, 一般情況下我們都會顯示的設置這個屬性, 這樣及時以後我們對該類進行了某些改動, 只要這個值保持一樣, JVM就還是會認為這個類文件是沒有變的。