JAVA 對象序列化——Serializable
1、序列化是幹什麽的?
簡單說就是為了保存在內存中的各種對象的狀態(也就是實例變量,不是方法),並且可以把保存的對象狀態再讀出來。雖然你可以用你自己的各種各樣的方法來保存object states,但是Java給你提供一種應該比你自己好的保存對象狀態的機制,那就是序列化。
2、什麽情況下需要序列化
a)當你想把的內存中的對象狀態保存到一個文件中或者數據庫中時候;
b)當你想用套接字在網絡上傳送對象的時候;
c)當你想通過RMI傳輸對象的時候;
3、當對一個對象實現序列化時,究竟發生了什麽?
在沒有序列化前,每個保存在堆(Heap)中的對象都有相應的狀態(state),即實例變量(instance ariable)比如:
- Foo myFoo = new Foo();
- myFoo .setWidth(37);
- myFoo.setHeight(70);
當 通過下面的代碼序列化之後,MyFoo對象中的width和Height實例變量的值(37,70)都被保存到foo.ser文件中,這樣以後又可以把它 從文件中讀出來,重新在堆中創建原來的對象。當然保存時候不僅僅是保存對象的實例變量的值,JVM還要保存一些小量信息,比如類的類型等以便恢復原來的對 象。
- FileOutputStream fs = new FileOutputStream("foo.ser");
- ObjectOutputStream os = new ObjectOutputStream(fs);
- os.writeObject(myFoo);
4、實現序列化(保存到一個文件)的步驟
a)Make a FileOutputStream
- FileOutputStream fs = new FileOutputStream("foo.ser");
b)Make a ObjectOutputStream
java 代碼- ObjectOutputStream os = new ObjectOutputStream(fs);
c)write the object
java 代碼- os.writeObject(myObject1);
- os.writeObject(myObject2);
- os.writeObject(myObject3);
d) close the ObjectOutputStream
java 代碼- os.close();
5、舉例說明
import java.io.*; public class Box implements Serializable { private int width; private int height; public void setWidth(int width){ this.width = width; } public void setHeight(int height){ this.height = height; } public static void main(String[] args){ Box myBox = new Box(); myBox.setWidth(50); myBox.setHeight(30); try{ FileOutputStream fs = new FileOutputStream("foo.ser"); ObjectOutputStream os = new ObjectOutputStream(fs); os.writeObject(myBox); os.close(); }catch(Exception ex){ ex.printStackTrace(); } } }
6、相關註意事項
a)序列化時,只對對象的狀態進行保存,而不管對象的方法;
b)當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable接口;
c)當一個對象的實例變量引用其他對象,序列化該對象時也把引用對象進行序列化;
d)並非所有的對象都可以序列化,,至於為什麽不可以,有很多原因了,比如:
1.安全方面的原因,比如一個對象擁有private,public等field,對於一個要傳輸的對象,比如寫到文件,或者進行rmi傳輸 等等,在序列化進行傳輸的過程中,這個對象的private等域是不受保護的。
2. 資源分配方面的原因,比如socket,thread類,如果可以序列化,進行傳輸或者保存,也無法對他們進行重新的資源分 配,而且,也是沒有必要這樣實現。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Java的對象序列化是指將那些實現了Serializable接口的對象轉換成一個字符序列,並能夠在以後將這個字節序列完全恢復為原來的對象。這一過程甚至可通過網絡進行,這意味著序列化機制能自動彌補不同操作系統之間的差異。 只要對象實現了Serializable接口(記住,這個接口只是一個標記接口,不包含任何的方法
如果我們想要序列化一個對象,首先要創建某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然後將這些OutputStream封裝在一個ObjectOutputStream中。這時候,只需要調用writeObject()方法就可以將對象序列化,並將其發送給OutputStream(記住:對象的序列化是基於字節的,不能使用Reader和Writer等基於字符的層次結構)。而飯序列的過程(即將一個序列還原成為一個對象),需要將一個InputStream(如FileInputstream、ByteArrayInputStream等)封裝在ObjectInputStream內,然後調用readObject()即可。
對象序列化過程不僅僅保存單個對象,還能追蹤對象內所包含的所有引用,並保存那些對象(這些對象也需實現了Serializable接口)。下面這段代碼演示了此過程:
package test.serializable; /** *@chenfeic * *只是一個簡單的類,用於測試序列化 */ import java.io.Serializable; public class Data implements Serializable { private static final long serialVersionUID = 7247714666080613254L; public int n; public Data(int n) { this.n = n; } public String toString(){ return Integer.toString(n); } }
package test.serializable; import java.io.Serializable; import java.util.Random; /** * * @author chenfei * * 用於測試序列化,每個對象Worm對象都與worm中的下一段鏈接,同時又有屬於不同類(Data)的對象引用數組鏈接 */ public class Worm implements Serializable { private static final long serialVersionUID = 5468335797443850679L; private Data[] d = { new Data(random.nextInt(10)), new Data(random.nextInt(10)), new Data(random.nextInt(10)) }; private static Random random = new Random(47); 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 sb = new StringBuilder(":"); sb.append(c); sb.append("("); for(Data data : d) { sb.append(data); } sb.append(")"); if(next!=null) { sb.append(next); } return sb.toString(); } }
package test.serializable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class SerializableTest { public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { Worm w = new Worm(6 ,‘a‘); System.out.println("序列化操縱之前"); System.out.println("w="+w); //序列化操作1--FileOutputStream ObjectOutputStream oos1 = new ObjectOutputStream(new FileOutputStream("worm.out")); oos1.writeObject("Worm storage By FileOutputStream "); oos1.writeObject(w);//必須所有引用的對象都實現序列化(本例終究是Data這個類),否則拋出有java.io.NotSerializableException:這個異常 oos1.close(); //反序列化操作1---FileInputStream ObjectInputStream ois1 = new ObjectInputStream(new FileInputStream("worm.out")); String s1 = (String)ois1.readObject(); Worm w1 = (Worm)ois1.readObject(); ois1.close(); System.out.println("反序列化操作1之後"); System.out.println(s1); System.out.println("w1:"+w1); //序列化操作2--ByteArrayOutputStream ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(); ObjectOutputStream oos2 = new ObjectOutputStream(byteOutStream); oos2.writeObject("Worm storage By ByteOutputStream "); oos2.writeObject(w); oos2.flush(); //反序列操作2--ByteArrayInputStream ByteArrayInputStream byteInStream = new ByteArrayInputStream(byteOutStream.toByteArray()); ObjectInputStream ois2 = new ObjectInputStream(byteInStream); String s2 = (String)ois2.readObject(); Worm w2 = (Worm)ois2.readObject(); ois2.close(); System.out.println("反序列化操作2之後"); System.out.println(s2); System.out.println("w2:"+w2); } }
運行的結果如下:
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) 反序列化操作1之後 Worm storage By FileOutputStream w1::a(853):b(119):c(802):d(788):e(199):f(881) 反序列化操作2之後 Worm storage By ByteOutputStream w2::a(853):b(119):c(802):d(788):e(199):f(881)
思考:
1)反序列化後的對象,需要調用構造函數重新構造嗎?
答案:不需要。對於Serializable對象,對象完全以它存儲的二進制位作為基礎來構造,而不調用構造器。
請看下面這段代碼
package test.serializable; import java.io.Serializable; import java.util.Date; /** * * @author chenfei * * 用於測試序列化時的deep copy * */ public class House implements Serializable { private static final long serialVersionUID = -6091530420906090649L; private Date date = new Date(); //記錄當前的時間 public String toString() { return "House:" + super.toString() + ".Create Time is:" + date; } }
package test.serializable; import java.io.Serializable; public class Animal implements Serializable { private static final long serialVersionUID = -213221189192962074L; private String name; private House house; public Animal(String name , House house) { this.name = name; this.house = house; System.out.println("調用了構造器"); } public String toString() { return name + "[" +super.toString() + "‘]" + house; } }
package test.serializable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Myworld { /** * @param args * @throws IOException * @throws ClassNotFoundException */ public static void main(String[] args) throws IOException, ClassNotFoundException { House house = new House(); System.out.println("序列化前"); Animal animal = new Animal("test",house); ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(out); oos.writeObject(animal); oos.flush(); oos.close(); System.out.println("反序列化後"); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); ObjectInputStream ois = new ObjectInputStream(in); Animal animal1 = (Animal)ois.readObject(); ois.close(); } }
運行結果如下所示:
序列化前 調用了構造器 反序列化後
從上面的結果中可以看到,在序列化前,當我們使用
Animal animal = new Animal("test",house);
時,調用了Animal的構造器(打印了輸出語句),但是反序列後並沒有再打印任何語句,說明並沒有調用構造器。
2)序列前的對象與序列化後的對象是什麽關系?是("=="還是equal?是淺復制還是深復制?)
答案:深復制,反序列化還原後的對象地址與原來的的地址不同。 我們還是看上面思考1)中給出的代碼,前兩個類不變化,修改第三個類(MyWorld.java)的部分代碼,修改後的代碼如下:
package test.serializable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Myworld { /** * @param args * @throws IOException * @throws ClassNotFoundException */ public static void main(String[] args) throws IOException, ClassNotFoundException { House house = new House(); System.out.println("序列化前"); Animal animal = new Animal("test",house); System.out.println(animal); ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(out); oos.writeObject(animal); oos.writeObject(animal);//在寫一次,看對象是否是一樣, oos.flush(); oos.close(); ByteArrayOutputStream out2 = new ByteArrayOutputStream();//換一個輸出流 ObjectOutputStream oos2 = new ObjectOutputStream(out2); oos2.writeObject(animal); oos2.flush(); oos2.close(); System.out.println("反序列化後"); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); ObjectInputStream ois = new ObjectInputStream(in); Animal animal1 = (Animal)ois.readObject(); Animal animal2 = (Animal)ois.readObject(); ois.close(); ByteArrayInputStream in2 = new ByteArrayInputStream(out2.toByteArray()); ObjectInputStream ois2 = new ObjectInputStream(in2); Animal animal3 = (Animal)ois2.readObject(); ois2.close(); System.out.println("out流:" +animal1); System.out.println("out流:" +animal2); System.out.println("out2流:" +animal3); System.out.println("測試序列化前後的對象 == :"+ (animal==animal1)); System.out.println("測試序列化後同一流的對象:"+ (animal1 == animal2)); System.out.println("測試序列化後不同流的對象==:" + (animal1==animal3)); } }
運行結果如下:
序列化前 調用了構造器 test[test.serializable.Animal@bb7465‘]House:[email protected] Time is:Sat Apr 06 00:11:30 CST 2013 反序列化後 out流:test[test.serializable.Animal@4f80d6‘]House:[email protected] Time is:Sat Apr 06 00:11:30 CST 2013 out流:test[test.serializable.Animal@4f80d6‘]House:[email protected] Time is:Sat Apr 06 00:11:30 CST 2013(與上面的相同) out2流:test[test.serializable.Animal@12cc95d‘]House:[email protected] Time is:Sat Apr 06 00:11:30 CST 2013(與上面只是值相同,但是地址不一樣。) 測試序列化前後的對象 == :false 測試序列化後同一流的對象:true 測試序列化後不同流的對象==:false
從結果可以看到
序列化前後對象的地址不同了,但是內容是一樣的,而且對象中包含的引用也相同。換句話說,通過序列化操作,我們可以實現對任何可Serializable對象的”深度復制(deep copy)"——這意味著我們復制的是整個對象網,而不僅僅是基本對象及其引用。對於同一流的對象,他們的地址是相同,說明他們是同一個對象,但是與其他流的對象地址卻不相同。也就說,只要將對象序列化到單一流中,就可以恢復出與我們寫出時一樣的對象網,而且只要在同一流中,對象都是同一個。
補充:
serialVersionUID 的作用?
在Java中,軟件的兼容性是一個大問題,尤其在使用到對象串行性的時候,那麽在某一個對象已經被串行化了,可是這個對象又被修改後重新部署了,那麽在這種情況下, 用老軟件來讀取新文件格式雖然不是什麽難事,但是有可能丟失一些信息。 serialVersionUID來解決這些問題,新增的serialVersionUID必須定義成下面這種形式:static final long serialVersionUID=-2805284943658356093L;。其中數字後面加上的L表示這是一個long值。 通過這種方式來解決不同的版本之間的串行話問題。
Java串行化機制定義的文件格式似乎很脆弱,只要稍微改動一下類的定義,原來保存的對象就可能無法讀取。例如,下面是一個簡單的類定義:
public class Save implements Serializable { String name; public void save() throws IOException { FileOutputStream f = new FileOutputStream("foo"); ObjectOutputStream oos = new ObjectOutputStream(f); oos.writeObject(this); oos.close(); } }
如果在這個類定義中增加一個域,例如final int val = 7;,再來讀取原來保存的對象,就會出現下面的異常:
java.io.InvalidClassException:
Save; local class incompatible:
stream classdesc serialVersionUID = -2805284943658356093,
local class serialVersionUID = 3419534311899376629
上例異常信息中的數字串表示類定義裏各種屬性的編碼值:
●類的名字(Save)。
●域的名字(name)。
●方法的名字(Save)。
●已實現的接口(Serializable)。
改動上述任意一項內容(無論是增加或刪除),都會引起編碼值變化,從而引起類似的異常警報。這個數字序列稱為“串行化版本統一標識符”(serial version universal identifier),簡稱UID。解決這個問題的辦法是在類裏面新增一個域serialVersionUID,強制類仍舊使用原來的UID。新增的域必須是:
●static:該域定義的屬性作用於整個類,而非特定的對象。
●final:保證代碼運行期間該域不會被修改。
●long:它是一個64位的數值。
也就是說,新增的serialVersionUID必須定義成下面這種形式:static final long serialVersionUID=-2805284943658356093L;。其中數字後面加上的L表示這是一個long值。
當然,改動之後的類不一定能夠和原來的對象兼容。例如,如果把一個域的定義從String改成了int,執行逆-串行化操作時系統就不知道如何處理該值,顯示出錯誤信息:java.io.InvalidClassException: Save; incompatible types for field name。
JAVA 對象序列化——Serializable