0 糖 0 脂 0 卡,Bestin me 輕汽蘇打氣泡水 12 瓶 19.9 元(減 50 元)
什麼是序列化、意義及使用場景
- 序列化:將物件寫入到IO流中
- 反序列化:從IO流中恢復物件
- 意義:序列化機制允許將實現序列化的Java物件轉換位位元組序列,這些位元組序列可以儲存在磁碟上,或通過網路傳輸,以達到以後恢復成原來的物件。序列化機制使得物件可以脫離程式的執行而獨立存在。
- 使用場景:所有可在網路上傳輸的物件都必須是可序列化的,比如RMI(remote method invoke,即遠端方法呼叫),傳入的引數或返回的物件都是可序列化的,否則會出錯;所有需要儲存到磁碟的java物件都必須是可序列化的。通常建議:程式建立的每個JavaBean類都實現Serializeable介面。
序列化實現方式
如果需要將某個物件儲存到磁碟上或者通過網路傳輸,那麼這個類應該實現Serializable介面或Externalizable介面。
1、Serializable
1.1 普通序列化
Serializable介面是一個標記介面,不用實現任何方法。一旦實現了此介面,該類的物件就是可序列化的。
- 序列化步驟:
-
步驟一:建立一個ObjectOutputStream輸出流
-
步驟二:呼叫ObjectOutputStream物件的writeObject輸出可序列化物件
1 public class Person implements Serializable { 2 privateString name; 3 private int age; 4 //我不提供無參構造器 5 public Person(String name, int age) { 6 this.name = name; 7 this.age = age; 8 } 9 10 @Override 11 public String toString() { 12 return "Person{" + 13 "name='" + name + '\'' + 14 ", age=" + age + 15'}'; 16 } 17 } 18 19 public class WriteObject { 20 public static void main(String[] args) { 21 try (//建立一個ObjectOutputStream輸出流 22 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"))) { 23 //將物件序列化到檔案s 24 Person person = new Person("wang", 23); 25 oos.writeObject(person); 26 } catch (Exception e) { 27 e.printStackTrace(); 28 } 29 } 30 }
直接執行該程式,即可看到當前路徑下生成一個person.txt的檔案,裡面儲存的是二進位制位元組碼格式的資料。
- 反序列化步驟:
-
步驟一:建立一個ObjectInputStream輸入流
-
步驟二:呼叫ObjectInputStream物件的readObject()得到序列化的物件
我們將上面序列化到person.txt的person物件反序列化回來。
1 public class Person implements Serializable { 2 private String name; 3 private int age; 4 //我不提供無參構造器 5 public Person(String name, int age) { 6 System.out.println("反序列化,你呼叫我了嗎?"); 7 this.name = name; 8 this.age = age; 9 } 10 11 @Override 12 public String toString() { 13 return "Person{" + 14 "name='" + name + '\'' + 15 ", age=" + age + 16 '}'; 17 } 18 } 19 20 public class ReadObject { 21 public static void main(String[] args) { 22 try (//建立一個ObjectInputStream輸入流 23 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"))) { 24 Person per = (Person) ois.readObject(); 25 System.out.println(per); 26 } catch (Exception e) { 27 e.printStackTrace(); 28 } 29 } 30 }
執行該程式後,輸出
Person{name='wang', age=23}
並沒有輸出構造方法中標記的列印
"反序列化,你呼叫我了嗎?"
輸出告訴我們,反序列化並不會呼叫構造方法。反序列的物件是由JVM自己生成的物件,不是通過構造方法生成的。
1.2 成員是引用的序列化
如果一個可序列化的類的成員不是基本型別,也不是String型別,那這個引用型別也必須是可序列化的;否則,會導致此類不能序列化。
看例子,我們新增一個Teacher類。將Person去掉實現Serializable介面程式碼。
1 public class Person{ 2 //省略相關屬性與方法 3 } 4 public class Teacher implements Serializable { 5 6 private String name; 7 private Person person; 8 9 public Teacher(String name, Person person) { 10 this.name = name; 11 this.person = person; 12 } 13 14 public static void main(String[] args) throws Exception { 15 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) { 16 Person person = new Person("wang", 20); 17 Teacher teacher = new Teacher("zhang", person); 18 oos.writeObject(teacher); 19 } 20 } 21 }
執行程式直接報錯,因為Person類的物件是不可序列化的,這導致了Teacher的物件不可序列化。
1.3 同一物件序列化多次的機制
同一物件序列化多次,會將這個物件序列化多次嗎?答案是否定的。
1 public class WriteTeacher { 2 public static void main(String[] args) throws Exception { 3 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) { 4 Person person = new Person("路飛", 20); 5 Teacher t1 = new Teacher("雷利", person); 6 Teacher t2 = new Teacher("紅髮香克斯", person); 7 //依次將4個物件寫入輸入流 8 oos.writeObject(t1); 9 oos.writeObject(t2); 10 oos.writeObject(person); 11 oos.writeObject(t2); 12 } 13 } 14 }
依次將t1、t2、person、t2物件序列化到檔案teacher.txt檔案中。
注意:反序列化的順序與序列化時的順序一致。
1 public class ReadTeacher { 2 public static void main(String[] args) { 3 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) { 4 Teacher t1 = (Teacher) ois.readObject(); 5 Teacher t2 = (Teacher) ois.readObject(); 6 Person p = (Person) ois.readObject(); 7 Teacher t3 = (Teacher) ois.readObject(); 8 System.out.println(t1 == t2); 9 System.out.println(t1.getPerson() == p); 10 System.out.println(t2.getPerson() == p); 11 System.out.println(t2 == t3); 12 System.out.println(t1.getPerson() == t2.getPerson()); 13 } catch (Exception e) { 14 e.printStackTrace(); 15 } 16 } 17 } 18 //輸出結果 19 //false 20 //true 21 //true 22 //true 23 //true
從輸出結果可以看出,多次Java序列化同一物件,並不會將此物件序列化多次得到多個物件。
- Java序列化演算法
-
所有儲存到磁碟的物件都有一個序列化編碼號
-
當程式試圖序列化一個物件時,會先檢查此物件是否已經序列化過,只有此物件從未(在此虛擬機器)被序列化過,才會將此物件序列化為位元組序列輸出。
-
如果此物件已經序列化過,則直接輸出編號即可。
如圖所示
1.4 java序列化演算法潛在的問題
由於java序利化演算法不會重複序列化同一個物件,只會記錄已序列化物件的編號。如果序列化一個可變物件(物件內的內容可更改)後,更改了物件內容後再次序列化,並不會再次將此物件轉換為位元組序列,而只是儲存序列化編號。
import java.io.*; public class WriteObject { public static void main(String[] args) { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt")); ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) { //第一次序列化person Person person = new Person("9龍", 23); oos.writeObject(person); System.out.println(person); //修改name person.setName("海賊王"); System.out.println(person); //第二次序列化person oos.writeObject(person); //依次反序列化出p1、p2 Person p1 = (Person) ios.readObject(); Person p2 = (Person) ios.readObject(); System.out.println(p1 == p2); System.out.println(p1.getName()); System.out.println(p2.getName()); System.out.println(p1.getName().equals(p2.getName())); } catch (Exception e) { e.printStackTrace(); } } }View Code
執行程式,輸出結果
反序列化,你呼叫我了嗎? Person{name='9龍', age=23} Person{name='海賊王', age=23} true 9龍 9龍 true
可見,修改後的屬性被沒有被序列化到磁碟檔案中。
1.5 可選的自定義序列化
有些時候,我們有這樣的需求,某些屬性不需要序列化。使用transient關鍵字選擇不需要序列化的欄位。
public class Person implements Serializable { //不需要序列化名字與年齡 private transient String name; private transient int age; private int height; private transient boolean singlehood; public Person(String name, int age) { this.name = name; this.age = age; } //省略get,set方法 } public class TransientTest { public static void main(String[] args) throws Exception { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt")); ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) { Person person = new Person("9龍", 23); person.setHeight(185); System.out.println(person); oos.writeObject(person); Person p1 = (Person)ios.readObject(); System.out.println(p1); } } } //輸出結果 //Person{name='9龍', age=23', singlehood=true', height=185cm} //Person{name='null', age=0', singlehood=false', height=185cm}View Code
從輸出我們看到,使用transient修飾的屬性,java序列化時,會忽略掉此欄位,所以反序列化出的物件,被transient修飾的屬性是預設值。對於引用型別,值是null;基本型別,值是0;boolean型別,值是false。
使用transient雖然簡單,但將此屬性完全隔離在了序列化之外。java提供了可選的自定義序列化。可以進行控制序列化的方式,或者對序列化資料進行編碼加密等。
private void writeObject(java.io.ObjectOutputStream out) throws IOException; private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException; private void readObjectNoData() throws ObjectStreamException;
通過重寫writeObject與readObject方法,可以自己選擇哪些屬性需要序列化, 哪些屬性不需要。如果writeObject使用某種規則序列化,則相應的readObject需要相反的規則反序列化,以便能正確反序列化出物件。這裡展示對名字進行反轉加密。
public class Person implements Serializable { private String name; private int age; //省略構造方法,get及set方法 private void writeObject(ObjectOutputStream out) throws IOException { //將名字反轉寫入二進位制流 out.writeObject(new StringBuffer(this.name).reverse()); out.writeInt(age); } private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{ //將讀出的字串反轉恢復回來 this.name = ((StringBuffer)ins.readObject()).reverse().toString(); this.age = ins.readInt(); } }
當序列化流不完整時,readObjectNoData()方法可以用來正確地初始化反序列化的物件。例如,使用不同類接收反序列化物件,或者序列化流被篡改時,系統都會呼叫readObjectNoData()方法來初始化反序列化的物件。
更徹底的自定義序列化
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
writeReplace:在序列化時,會先呼叫此方法,再呼叫writeObject方法。此方法可將任意物件代替目標序列化物件
public class Person implements Serializable { private String name; private int age; //省略構造方法,get及set方法 private Object writeReplace() throws ObjectStreamException { ArrayList<Object> list = new ArrayList<>(2); list.add(this.name); list.add(this.age); return list; } public static void main(String[] args) throws Exception { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt")); ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) { Person person = new Person("9龍", 23); oos.writeObject(person); ArrayList list = (ArrayList)ios.readObject(); System.out.println(list); } } } //輸出結果 //[9龍, 23]
readResolve:反序列化時替換反序列化出的物件,反序列化出來的物件被立即丟棄。此方法在readeObject後呼叫。
public class Person implements Serializable { private String name; private int age; //省略構造方法,get及set方法 private Object readResolve() throws ObjectStreamException{ return new ("brady", 23); } public static void main(String[] args) throws Exception { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt")); ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) { Person person = new Person("9龍", 23); oos.writeObject(person); HashMap map = (HashMap)ios.readObject(); System.out.println(map); } } } //輸出結果 //{brady=23}
readResolve常用來反序列單例類,保證單例類的唯一性。
注意:readResolve與writeReplace的訪問修飾符可以是private、protected、public,如果父類重寫了這兩個方法,子類都需要根據自身需求重寫,這顯然不是一個好的設計。通常建議對於final修飾的類重寫readResolve方法沒有問題;否則,重寫readResolve使用private修飾。
2、Externalizable:強制自定義序列化
通過實現Externalizable介面,必須實現writeExternal、readExternal方法。
public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; }
public class ExPerson implements Externalizable { private String name; private int age; //注意,必須加上pulic 無參構造器 public ExPerson() { } public ExPerson(String name, int age) { this.name = name; this.age = age; } @Override public void writeExternal(ObjectOutput out) throws IOException { //將name反轉後寫入二進位制流 StringBuffer reverse = new StringBuffer(name).reverse(); System.out.println(reverse.toString()); out.writeObject(reverse); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { //將讀取的字串反轉後賦值給name例項變數 this.name = ((StringBuffer) in.readObject()).reverse().toString(); System.out.println(name); this.age = in.readInt(); } public static void main(String[] args) throws IOException, ClassNotFoundException { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt")); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"))) { oos.writeObject(new ExPerson("brady", 23)); ExPerson ep = (ExPerson) ois.readObject(); System.out.println(ep); } } } //輸出結果 //ydarb //brady //ExPerson{name='brady', age=23}
注意:Externalizable介面不同於Serializable介面,實現此介面必須實現介面中的兩個方法實現自定義序列化,這是強制性的;特別之處是必須提供pulic的無參構造器,因為在反序列化的時候需要反射建立物件。
3、兩種序列化對比
實現Serializable介面 | 實現Externalizable介面 |
---|---|
系統自動儲存必要的資訊 | 程式設計師決定儲存哪些資訊 |
Java內建支援,易於實現,只需要實現該介面即可,無需任何程式碼支援 | 必須實現介面內的兩個方法 |
效能略差 | 效能略好 |
雖然Externalizable介面帶來了一定的效能提升,但變成複雜度也提高了,所以一般通過實現Serializable介面進行序列化。
序列化版本號serialVersionUID
我們知道,反序列化必須擁有class檔案,但隨著專案的升級,class檔案也會升級,序列化怎麼保證升級前後的相容性呢?
java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即使更改了序列化屬性,物件也可以正確被反序列化回來。
public class Person implements Serializable { //序列化版本號 private static final long serialVersionUID = 1111013L; private String name; private int age; //省略構造方法及get,set }
如果反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常。
序列化版本號可自由指定,如果不指定,JVM會根據類資訊自己計算一個版本號,這樣隨著class的升級,就無法正確反序列化;不指定版本號另一個明顯隱患是,不利於jvm間的移植,可能class檔案沒有更改,但不同jvm可能計算的規則不一樣,這樣也會導致無法反序列化。
什麼情況下需要修改serialVersionUID呢?分三種情況。
- 如果只是修改了方法,反序列化不受影響,則無需修改版本號;
- 如果只是修改了靜態變數,瞬態變數(transient修飾的變數),反序列化不受影響,無需修改版本號;
- 如果修改了非瞬態變數,則可能導致反序列化失敗。如果新類中例項變數的型別與序列化時類的型別不一致,則會反序列化失敗,這時候需要更改serialVersionUID。如果只是新增了例項變數,則反序列化回來新增的變數是預設值;如果減少了例項變數,反序列化時會忽略掉減少的例項變數。
總結
- 所有需要網路傳輸的物件都需要實現序列化介面,通過建議所有的javaBean都實現Serializable介面。
- 物件的類名、例項變數(包括基本型別,陣列,對其他物件的引用)都會被序列化;方法、類變數、transient例項變數都不會被序列化。
- 如果想讓某個變數不被序列化,使用transient修飾。
- 序列化物件的引用型別成員變數,也必須是可序列化的,否則,會報錯。
- 反序列化時必須有序列化物件的class檔案。
- 當通過檔案、網路來讀取序列化後的物件時,必須按照實際寫入的順序讀取。
- 單例類序列化,需要重寫readResolve()方法;否則會破壞單例原則。
- 同一物件序列化多次,只有第一次序列化為二進位制流,以後都只是儲存序列化編號,不會重複序列化。
- 建議所有可序列化的類加上serialVersionUID 版本號,方便專案升級。