對象的序列化(Serialize):將內存中的Java對象轉換為與平台無關的二進制流(字節序列),然后存儲在磁盤文件中,或通過網絡傳輸給另一個網絡節點。
對象的反序列化(Deserialize):獲取序列化的二進制流(不管是通過網絡,還是通過讀取磁盤文件),將之恢復為原來的Java對象。
要實現對象的序列化,該對象所屬的類必須要是可序列化的,即該類必須實現以下2個接口之一:
- Serializable 這只是一個標記接口,此接口只是表明該類是可序列化的,不用實現任何方法。Java自帶的類基本都已implement Serializable,不用我們操心,我們自定義的類 要實現序列化,必須要手寫implement Serializable。
- Externalizable 用於實現自定義序列化
要通過網絡傳輸的Java對象、要保存到磁盤的Java對象必須要是可序列化的,不然程序會出現異常。
JavaEE的分布式應用往往要跨平台、跨網絡,通過網絡傳輸的Java對象必須要是可序列化的,HttpSession、ServletContext等Java Web對象都已實現序列化。如果我們要通過網絡傳輸自定義的Java對象,該類(一般是JavaBean)要是可序列化的。通常建議:把所有的JavaBean都寫成是可序列化的。
ObjectOutputStream是一個處理流,提供了void writeObject(Object obj)方法用於對象的序列化(對象輸出流,輸出到文件/網絡)。
ObjectInputStream也是一個處理流,提供了Object readObject()方法用於反序列化(對象輸入流,讀取文件/網絡中的對象到內存)。
示例:
1 //要實現Serializable接口(並不用實現任何方法) 2 class Student implements Serializable{ 3 private int id; 4 private String name; 5 private int age; 6 7 public Student(int id,String name,int age){ 8 this.id=id; 9 this.name=name; 10 this.age=age; 11 } 12 13 public int getId(){ 14 return this.id; 15 } 16 17 public void setId(int id){ 18 this.id=id; 19 } 20 21 public String getName(){ 22 return this.name; 23 } 24 25 public void setName(String name){ 26 this.name=name; 27 } 28 29 public int getAge(){ 30 return this.age; 31 } 32 33 public void setAge(int age){ 34 this.age=age; 35 }
1 //序列化 2 ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("./obj.txt")); //處理流,要建立在節點流之上 3 Student zhangsan=new Student(1,"張三",20); 4 oos.writeObject(zhangsan); //writeObject(Object obj)用於序列化一個對象(輸出到文件/網絡節點) 5 6 //反序列化 7 ObjectInputStream ois=new ObjectInputStream(new FileInputStream("./obj.txt")); 8 Student student=(Student)ois.readObject(); //返回的是Object類型,所以往往要強制類型轉換 9 10 System.out.println("學號:"+student.getId()); 11 System.out.println("姓名:"+student.getName()); 12 System.out.println("年齡:"+student.getAge());
ObjectInputStream、ObjectOutputStream也包含了其他IO方法,可輸入/輸出多種類型的數據,即可以字符為單位進行操作,又可以字節為單位進行操作。
writeObject(Object obj)一次只能寫出一個對象,可多次調用,向同一文件中寫入多個對象;
readObject()一次只能讀一個對象,讀取一個對象后,指針自動后移,指向下一個對象。讀的順序和寫的順序是一一對應的。
示例:
1 //序列化,向同一文件中寫入多個對象 2 ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("./obj.txt")); //處理流,要建立在節點流之上 3 Student zhangsan=new Student(1,"張三",20); 4 Student lisi=new Student(2,"李四",20); 5 Student wangwu=new Student(3,"王五",20); 6 oos.writeObject(zhangsan); //張三 7 oos.writeObject(lisi); //李四 8 oos.writeObject(wangwu); //王五 9 10 //反序列化,讀取順序和寫入順序是一致的 11 ObjectInputStream ois=new ObjectInputStream(new FileInputStream("./obj.txt")); 12 Student zhangsan1=(Student)ois.readObject(); //張三 13 Student lisi1=(Student)ois.readObject(); //李四 14 Student wangwu1=(Student)ois.readObject(); //王五
如果要序列化的類有父類(直接或者間接),那么這些父類必須要是可序列化的,或者具有無參的構造函數,否則會拋出 InvalidClassException 異常。
1 class Student extends People implements Serializable{ 2 //...... 3 }
如果Student要序列化,則有2種方案可選:
- 其父類People也要是可序列化的(遞歸)。 這樣Student繼承自People中的成員才可以序列化輸出到文件/網絡。
- 其父類不是可序列化的,但是其父類有無參的構造函數。 這樣不會報錯,但Student繼承自People中的成員不會序列化輸出到文件/網絡。
如果該類持有其它類的引用,則引用的類都要是可序列化的,此類才是可序列化的。
1 class Student implements Serializable { 2 private int id; 3 private String name; //Java自帶的類基本都實現了可序列化,不用管 4 private int age; 5 private Teacher teacher; //此類持有一個Teacher類的引用。Teacher類必須要是可序列化的,Student類才是可序列化的。 6 //...... 7 }
如果要多次輸出同一個對象,則第一次輸出的是該對象的字節序列,以后每次輸出的是該對象的編號。
1 ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("./obj.txt")); 2 Student zhangsan=new Student(1,"張三",20); 3 4 oos.writeObject(zhangsan); //第一次序列化這個對象,輸出此對象的字節序列。假設此對象的編號為1,編號唯一標識此對象。 5 oos.writeObject(zhangsan); //再次輸出此對象,輸出的是此對象的編號,直接輸出1。讀取此對象時讀取的仍是zhangsan這個對象。 6 oos.writeObject(zhangsan); //直接輸出1。 7 //類似於c++中的指針,通過編號指向某個已存在對象。減少了重新序列化對象的時間、內存開銷。
如果中間修改了此對象,再次序列化此對象時,修改后的結果不會同步到文件/網絡節點中。
1 ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("./obj.txt")); 2 Student zhangsan=new Student(1,"張三",20); 3 4 oos.writeObject(zhangsan); //初次序列化,輸出此對象的字節序列。假設編號為1 5 zhangsan.setName("李四"); //之后修改了此對象 6 oos.writeObject(zhangsan); //再次輸出此對象,輸出的是編號1。1代表編號為1的對象。 7 8 /* 9 在此對象第一次序列化之后,對此對象做修改,后面再次輸出此對象時,都是指向第一次輸出的那個對象,做的修改並不會寫到文件/網絡節點中。 10 讀取的也都是第一次輸出的那個對象。 11 12 實際上,序列化一個對象時,JVM會先檢查在本次虛擬機中是否已序列化過這個對象,如果已序列化過,直接輸出對應的序列化編號,未序列化過才序列化。 13 JVM是以對象名來區分序列化的是否是同一個對象。對象名相同,JVM就認為序列化的是同一個對象,直接輸出該對象的編號,並不判斷此對象是否修改過。
14
15 修改此對象后,就算我們把此對象序列化到另一個文件中,輸出的也是此對象第一次序列化時的編號,修改並不會同步到這個文件中。
16 */
Java9新增了過濾功能,讀取對象時,可以先檢查該對象是否滿足指定的要求,滿足才允許恢復,否則拒絕恢復。
1 ObjectInputStream ois=new ObjectInputStream(new FileInputStream("obj.txt")); 2 /* 3 參數是一個ObjectInputFilter接口的對象,函數式接口,只需實現checkInput()方法。 4 參數info是FilterInfo類的對象。 5 6 返回值要是預定義的常量: 7 Status.ALLOWED 允許恢復 8 Status.REJECTED 拒絕恢復 9 Status.UNDECIDED 未決定,將繼續執行檢查 10 */ 11 ois.setObjectInputFilter((info)->{ 12 ObjectInputFilter serialFilter=ObjectInputFilter.Config.getSerialFilter(); //獲取默認的輸入過濾器 13 if(serialFilter!=null){ 14 //先使用默認的過濾器進行檢查 15 ObjectInputFilter.Status status=serialFilter.checkInput(info); 16 //如果已知道結果 17 if (status!= ObjectInputFilter.Status.UNDECIDED) 18 return status; 19 } 20 //執行到此,就說明結果是Status.UNDECIDED,還不知道結果,要繼續檢查 21 //如果引用不唯一,說明數據被污染了,不允許恢復 22 if(info.references()!=1) 23 return ObjectInputFilter.Status.REJECTED; 24 //是指定的類的對象(Student),才允許恢復(執行反序列化) 25 if(info.serialClass()!=null && info.serialClass() != Student.class) 26 return ObjectInputFilter.Status.ALLOWED; 27 //執行到此,說明不是指定類的對象,不允許恢復。 28 return ObjectInputFilter.Status.REJECTED; 29 }); 30 //這樣恢復的對象都是唯一的Student對象 31 //...........