在Java的世界裏,建立好對象以後,只要須要,對象是能夠長駐內存,可是在程序終止時,全部對象仍是會被銷燬。這其實很合理,可是即便合理也不必定能知足全部場景,仍然存在着一些狀況,須要可以在程序不運行的狀況下保持對象,因此序列化機制應運而生。java
簡單來講序列化的做用就是將內存中的對象保存起來,在須要時能夠重建該對象,而且重建後的對象擁有與保存以前的對象所擁有的信息相同。在實際應用中,對象序列化常會用在以下場景:數據庫
也許你會以爲,要達到這種持久化的效果,咱們直接將信息寫入文件或數據庫也能夠實現啊,爲何還要序列化?這是一個好問題,試想若是咱們採用前面所述的方法,在序列化對象和反序列化恢復對象時,咱們必須考慮如何完整的保存和恢復對象的信息,這裏面會涉及到不少繁瑣的細節,稍加不注意就可能致使信息的丟失。若是可以有一種機制,只要將一個對象聲明爲是「持久性」的,就可以爲咱們處理掉全部細節,這樣豈不是很方便,這就是序列化要作的事情。Java已經將序列化的概念加入到語言中,本文的關於序列化的全部例子都是基於Java的。數組
Java提供的原生序列化機制功能強大,有其本身的一些特色:網絡
Java的對象序列化機制是將那些實現了Serializable接口的對象轉換成一個字節序列,並可以在之後將這個字節序列徹底恢復爲原來的對象。app
要序列化一個對象,首先要建立一個OutputStream對象,而後將其封裝在一個ObjectOutputStream對象內,接着只需調用writeObject()方法便可將對象序列化,並將序列化後的字節序列發送給OutputStream。要將一個序列還原爲一個對象,則須要將一個InputStream封裝在ObjectInputStream內,而後調用readObject(),該方法會返回一個引用,它指向一個向上轉型的Object,必須向下轉型才能直接使用。框架
咱們來看一個例子,如何序列化和反序列化對象。dom
public class Worm implements Serializable{ private static Random rand = new Random(47); private Data[] d = { new Data(rand.nextInt(10)), new Data(rand.nextInt(10)), new Data(rand.nextInt(10)) }; private Worm next; private char c; public Worm(int i, char x){ System.out.println("Worm constructor: " + i); c = x; if(--i > 0){ next = new Worm(i,(char)(x + 1)); } } public Worm(){ System.out.println("Default constructor"); } public String toString(){ StringBuilder result = new StringBuilder(":"); result.append(c); result.append("("); for(Data dat : d){ result.append(dat); } result.append(")"); if(next != null){ result.append(next); } return result.toString(); } public static void main(String[] args) throws ClassNotFoundException, IOException{ Worm w = new Worm(6,'a'); System.out.println("w = " + w); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out")); out.writeObject("Worm storage\n"); out.writeObject(w); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out")); String s = (String)in.readObject(); Worm w2 = (Worm)in.readObject(); System.out.println(s + "w2 = " + w2); ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out2 = new ObjectOutputStream(bout); out2.writeObject("Worm storage\n"); out2.writeObject(w); out2.flush(); ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray())); s = (String)in2.readObject(); Worm w3 = (Worm)in2.readObject(); System.out.println(s + "w3 = " + w3); } } class Data implements Serializable{ private int n; public Data(int n){this.n = n;} public String toString(){return Integer.toString(n);} }
輸出結果以下:函數
Worm constructor: 6 Worm constructor: 5 Worm constructor: 4 Worm constructor: 3 Worm constructor: 2 Worm constructor: 1 w = :a(853):b(119):c(802):d(788):e(199):f(881) Worm storage w2 = :a(853):b(119):c(802):d(788):e(199):f(881) Worm storage w3 = :a(853):b(119):c(802):d(788):e(199):f(881)
這段代碼經過對連接的對象生成一個worm(蠕蟲)對序列化機制進行測試,每一個對象都與worm中的下一段連接,同時又與屬於不一樣類(Data)的對象引用數組連接。測試
對象序列化不只保存了對象的「全景圖」,並且能追蹤對象內所包含的全部引用,並保存那些對象;還能對對象內包含的每一個這樣的引用進行追蹤;依此類推。ui
並且從上面的輸出結果還能夠看出一個Serializable對象進行還原的過程當中,沒有調用任何構造器,包括默認的構造器。整個對象都是經過從InputStream中取得數據恢復而來的。
前面咱們有說到序列化的目的之一是支持rpc框架的數據傳輸,好比咱們將一個對象序列化,並經過網絡將其傳給另外一臺計算機,另外一臺計算機經過反序列化來還原這個對象,那是否只須要該序列化文件就能還原對象呢?咱們用下面的代碼來測試一下。
public class Serialize implements Serializable{} } public class FreezeSerialize { public static void main(String[] args) throws Exception{ ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("X.file")); Serialize serialize = new Serialize(); os.writeObject(alien); } } public class ThawSerialize { public static void main(String[] args) throws Exception{ ObjectInputStream in = new ObjectInputStream(new FileInputStream("X.file")); Object mystery = in.readObject(); System.out.println(mystery.getClass()); } }
FreezeSerialize類用來把對象序列化到文件中,ThawSerialize類用來反序列化對象,在測試電腦上若是同時執行這兩個類是沒有問題的,但若是咱們將Serialize類的位置更改一下(或者直接將FreezeSerialize和Serialize刪掉),則執行ThawSerialize反序列化時會報錯誤--ClassNotFoundException,因此能夠知道,反序列化時是須要原對象的Class對象的。
既然反序列化時須要對應的Class對象,那若是序列化時和反序列化時對應的Class版本不同會怎麼樣呢(這種狀況是存在的)?爲了模擬這種狀況,咱們先執行FreezeSerialize類的main方法,再給Serialize類添加一個屬性,這時再跑一下ThawSerialize類的main方法,能夠發現報java.io.InvalidClassException異常,明明是能經過編譯的可是卻報錯了,這種狀況有沒有什麼辦法解決呢?有的,咱們能夠給實現了Serializable接口的類添加一個long類型的成員:serialVersionUID,修飾符爲private static final,而且指定一個隨機數便可。
這個serialVersionUID其實叫序列化版本號,若是不指定的話,編譯器會在編譯後的class文件中默認添加一個,其值是根據當前類結構生成。可是這樣會帶來一個問題,若是類的結構發生了改變,那編譯以後對應的版本號也會發生改變,而虛擬機是否容許反序列化,不只取決於類路徑和功能代碼是否一致,還有一個很是重要的一點是兩個類的序列化ID是否一致,若是不一致則不容許序列化而且會拋出InvalidClassException異常,這就是前面不添加序列號時更改類結構再反序列化時會報錯的緣由。因此建議給實現了Serializable接口的類添加一個序列化版本號serialVersionUID,並指定值。
關於序列化版本號還有一個點須要主意,版本號一致的狀況下,若待反序列化的對象與當前類現有結構不一致時,則採用兼容模式,即:該對象的屬性現有類有的則還原,沒有的則忽略。
上面咱們咱們使用的是Java提供的默認序列化機制,即將對象成員所有序列化。可是,若是有特殊的須要呢?好比,只但願對象的某一部分被序列化。在這種特殊狀況下,能夠經過若干種方法來實現想要的效果,下面一一介紹。
經過實現Externalizable接口(代替實現Serializable),能夠對序列化過程進行控制。這個Externalizable接口繼承了Serializable接口,同時增長了兩個方法:writeExternal()和readExternal()。這兩個方法會分別在序列化和反序列化還原的過程當中被自動調用,這樣就能夠在這兩個方法種指定執行一些特殊操做。下面來看一個簡單例子:
public class Blip implements Externalizable{ private int i; private String s; public Blip(){ System.out.println("Blip Constructor"); } public Blip(String x, int a){ System.out.println("Blip(String x, int a)"); s = x; i = a; } public String toString(){ return s + i; } public void writeExternal(ObjectOutput out) throws IOException{ System.out.println("Blip.writeExternal"); out.writeObject(s); out.writeInt(i); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{ System.out.println("Blip.readExternal"); s = (String)in.readObject(); i = in.readInt(); } public static void main(String[] args) throws IOException, ClassNotFoundException{ System.out.println("Constructing objects:"); Blip b = new Blip("A String ",47); System.out.println(b); // 序列化對象 ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Blip.out")); System.out.println("Saving object:"); o.writeObject(b); o.close(); // 還原對象 ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip.out")); System.out.println("Recovering b:"); b = (Blip)in.readObject(); System.out.println(b); } }
在這個例子中,對象繼承了Externalizable接口,成員s和i只在第二個構造器中初始化(而不是默認構造器),咱們在writeExternal()方法中對要序列化保存的成員執行寫入操做,在readExternal()方法中將其恢復。輸出結果以下:
Constructing objects: Blip(String x, int a) A String 47 Saving object: Blip.writeExternal Recovering b: Blip Constructor Blip.readExternal A String 47
這裏須要注意的幾個點:
在咱們對序列化進行控制時,可能會碰到某個特定子對象不想讓Java的序列化機制自動保存與恢復。好比一些敏感信息(密碼),即便對象中的這些成員是由private修飾,一經序列化處理,經過讀取文件或者網絡抓包的方式仍是能訪問到它。
前面說的經過實現Externalizable接口能夠解決這個問題,可是假如對象有不少的成員,而咱們只但願其中少許成員不被序列化,那經過實現Externalizable接口的方式就不合適了(由於須要在writeExternal()方法中作大量工做),這種狀況下,transient關鍵就能夠大顯身手了。在實現了Serializable接口的類中,被transient關鍵字修飾的成員是不會被序列化的。並且,因爲Externalizable對象在默認狀況下不會序列化對象的任何字段,transient關鍵字只能和Serializable對象一塊兒使用。
除了上面兩種方法,還有一種相對不那麼「正規」的辦法--咱們能夠實現Serializable接口,並添加名爲writeObject()和readObject()的方法。當對象被序列化或者被反序列化還原時,就會自動地分別調用這兩個方法(只要咱們提供了這兩個方法,就會使用它們而不是默認的序列化機制)。可是須要注意的是這兩個方法必須有準確的方法特徵簽名:
private void writeObject(ObjectOutputStream stream) throws IOException; private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;
在調用ObjectOutputStream.writeObject()時,會檢查所傳遞的Serializable對象,看看是否實現了它本身的writeObject()。若是是,就跳過正常的序列化過程並調用對象本身的writeObject()方法。readObject()的狀況是相似的。這就是這種方式的原理。
還有一個技巧,在咱們提供的writeObject()內部,能夠調用defaultWriteObject()來選擇執行默認的writeObject()。相似,在readObject()內部,咱們能夠調用defaultReadObject()。下面看一個例子,如何對一個Serializable對象的序列化與恢復進行控制:
public class SerialCtl implements Serializable{ private String noTran; private transient String tran; public SerialCtl(String noTran, String tran){ this.noTran = "Not Transient: " + noTran; this.tran = "Transient: " + tran; } public String toString(){ return noTran + "\n" + tran; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); stream.writeObject(tran); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException{ stream.defaultReadObject(); tran = (String)stream.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException{ SerialCtl sc = new SerialCtl("papaya","mango"); System.out.println("Before:\n" + sc); ByteArrayOutputStream buf = new ByteArrayOutputStream(); ObjectOutputStream o = new ObjectOutputStream(buf); o.writeObject(sc); // 還原 ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray())); SerialCtl sc2 = (SerialCtl)in.readObject(); System.out.println("After:\n" + sc2); } }
輸出結果:
Before:
Not Transient: papaya
Transient: mango
After:
Not Transient: papaya
Transient: mango
在這個例子中,有一個String字段是普通字段,而另外一個是transient字段,對比證實非transient字段由defaultWriteObject()方法保存,而transient字段必須在程序中明確保存和恢復。
在writeObject()內部第一行調用defaultWriteObject()方法是爲了利用默認序列化機制序列化對象的非transient成員,一樣,在readObject()內部第一行調用defaultReadObject()方法是爲了利用默認機制恢復非transient成員。注意,必須是第一行調用。
使用序列化的一個主要目的是存儲程序的一些狀態,以便咱們後面能夠容易地將程序恢復到當前狀態。在這樣作以前,咱們先考慮幾種狀況。若是咱們將兩個對象(它們都包含有有指向第三個對象的引用成員)進行序列化,會發生什麼狀況?當咱們從它們的序列化文件中恢復這兩個對象時,第三個對象會只出現一次嗎?若是將這兩個對象序列化成獨立的文件,而後在代碼的不一樣部分對它們進行反序列化還原,又會怎樣呢?先看例子:
public class MyWorld { public static void main(String[] args) throws IOException, ClassNotFoundException{ House house = new House(); List<Animal> animals = new ArrayList<Animal>(); animals.add(new Animal("Bosco the dog", house)); animals.add(new Animal("Ralph the hamster", house)); animals.add(new Animal("Molly the cat",house)); System.out.println("animals: " + animals); ByteArrayOutputStream buf1 = new ByteArrayOutputStream(); ObjectOutputStream o1 = new ObjectOutputStream(buf1); o1.writeObject(animals); o1.writeObject(animals); // 寫入到另外一個流中: ByteArrayOutputStream buf2 = new ByteArrayOutputStream(); ObjectOutputStream o2 = new ObjectOutputStream(buf2); o2.writeObject(animals); // 反序列化: ObjectInputStream in1 = new ObjectInputStream(new ByteArrayInputStream(buf1.toByteArray())); ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(buf2.toByteArray())); List animals1 = (List)in1.readObject(),animals2 = (List)in1.readObject(),animals3 = (List)in2.readObject(); System.out.println("animals1: " + animals1); System.out.println("animals2: " + animals2); System.out.println("animals3: " + animals3); } } class House implements Serializable{} class Animal implements Serializable{ private String name; private House preferredHouse; Animal(String nm, House h){ name = nm; preferredHouse = h; } public String toString(){ return name + "[" + super.toString() + "]. " + preferredHouse + "\n"; } }
輸出結果:
animals: [Bosco the dog[testDemos.Animal@7852e922]. testDemos.House@4e25154f , Ralph the hamster[testDemos.Animal@70dea4e]. testDemos.House@4e25154f , Molly the cat[testDemos.Animal@5c647e05]. testDemos.House@4e25154f ] animals1: [Bosco the dog[testDemos.Animal@2d98a335]. testDemos.House@16b98e56 , Ralph the hamster[testDemos.Animal@7ef20235]. testDemos.House@16b98e56 , Molly the cat[testDemos.Animal@27d6c5e0]. testDemos.House@16b98e56 ] animals2: [Bosco the dog[testDemos.Animal@2d98a335]. testDemos.House@16b98e56 , Ralph the hamster[testDemos.Animal@7ef20235]. testDemos.House@16b98e56 , Molly the cat[testDemos.Animal@27d6c5e0]. testDemos.House@16b98e56 ] animals3: [Bosco the dog[testDemos.Animal@4f3f5b24]. testDemos.House@15aeb7ab , Ralph the hamster[testDemos.Animal@7b23ec81]. testDemos.House@15aeb7ab , Molly the cat[testDemos.Animal@6acbcfc0]. testDemos.House@15aeb7ab ]
這裏咱們經過一個字節數組來使用對象序列化,這樣能夠實現對任何可Serializable對象的「深度複製」(deep copy)--深度複製意味着複製的是整個對象網,而不只僅是基本對象及其引用。
在這個例子中,咱們從打印的結果能夠看出,只要將任何對象序列化到單一流中,就能夠恢復出與咱們寫入時同樣的對象網,而且不會有任何意外重複複製出的對象,對比animals1和animals2中的House。
另外一方面,在恢復animals3時,輸出的House與animals1和animals2是不一樣的,這說明了若是將對象序列化到不一樣的文件中,而後在代碼的不一樣部分對它們進行反序列化還原,這時會產生出兩個對象。
序列化的出現給保存程序運行狀態提供了一種新的途徑,實際主要使用在RPC框架的數據傳輸以及對象狀態的持久化保存等場景。