目錄
一、認識序列化
1、從網絡通信認識序列化
爲了很好的理解序列化,先不講概念,而是先從網絡通信談起,我們知道現在的網絡通信技術基本上都是基於TCP/IP來實現的。假設我們有兩臺電腦,這兩臺電腦之間寫好了java程序,一個是send端,一個是receive端,要實現他們的通信,其底層是怎麼實現的呢?請看下面這張圖。
從上面這張圖我們可以看到,兩個進程進行通信時候,想要發送數據,要先要把數據發送到TCP緩衝區,然後形成報文再發送出去,同樣的道理,接收端也是一樣。我們可以相互發送各種類型的數據,包括文本、圖片、音頻、視頻等, 而這些數據都會以二進制序列的形式在網絡上傳送。同樣的,當兩個Java進程進行通信時,也可以使用序列化技術實現對象之間的傳遞。
爲了理解起來方便,再來看一張圖。
從這張圖也可以清晰的看出,發送數據之前要序列化,接受數據要反序列化。到了這,我們再來看序列化的概念就比較好理解了,一句話:Java序列化是指把Java對象轉換爲字節序列的過程,而Java反序列化是指把字節序列恢復爲Java對象的過程;
2、序列化的使用場景
這個使用場景應該算是最重要的一環了,因爲我們學習序列化就是爲了使用他,現在把他們歸納一下:
(1)永久性保存對象,保存對象的字節序列到本地文件或者數據庫中;
(2)通過序列化以字節流的形式使對象在網絡中進行傳遞和接收;
(3)通過序列化在進程間傳遞對象;
3、序列化有什麼好處呢?
其實好處是根據使用場景來的;
(1)實現了數據的持久化,通過序列化可以把數據永久地保存到硬盤上
(2)利用序列化實現遠程通信,即在網絡上傳送對象的字節序列。
4、序列化技術都有哪些?
文字總是看着很枯燥,還是看圖吧。
看起來很多呀,不過後續的課程中,我會一個一個的講,或者是挑主要的,而且裏面我也沒有全用過,大概會六七種吧。這篇文章也主要看第一個java的序列化機制。
二、序列化實現的方式
如果需要將某個對象保存到磁盤上或者通過網絡傳輸,那麼這個類應該實現Serializable接口或者Externalizable接口之一。
1、Serializable
1.1 普通序列化
Serializable接口是一個標記接口,不用實現任何方法。一旦實現了此接口,該類的對象就是可序列化的。
- 序列化步驟:
-
步驟一:創建一個ObjectOutputStream輸出流;
-
步驟二:調用ObjectOutputStream對象的writeObject輸出可序列化對象。
public class Person implements Serializable { private String name; private int age; //我不提供無參構造器 public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class WriteObject { public static void main(String[] args) { try (//創建一個ObjectOutputStream輸出流 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) { //將對象序列化到文件s Person person = new Person("9龍", 23); oos.writeObject(person); } catch (Exception e) { e.printStackTrace(); } } }
- 反序列化步驟:
-
步驟一:創建一個ObjectInputStream輸入流;
-
步驟二:調用ObjectInputStream對象的readObject()得到序列化的對象。
我們將上面序列化到person.txt的person對象反序列化回來
public class Person implements Serializable { private String name; private int age; //我不提供無參構造器 public Person(String name, int age) { System.out.println("反序列化,你調用我了嗎?"); this.name = name; this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class ReadObject { public static void main(String[] args) { try (//創建一個ObjectInputStream輸入流 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"))) { Person brady = (Person) ois.readObject(); System.out.println(brady); } catch (Exception e) { e.printStackTrace(); } } } //輸出結果 //Person{name='9龍', age=23}
waht???? 輸出告訴我們,反序列化並不會調用構造方法。反序列的對象是由JVM自己生成的對象,不通過構造方法生成。
1.2 成員是引用的序列化
如果一個可序列化的類的成員不是基本類型,也不是String類型,那這個引用類型也必須是可序列化的;否則,會導致此類不能序列化。
看例子,我們新增一個Teacher類。將Person去掉實現Serializable接口代碼。
public class Person{
//省略相關屬性與方法
}
public class Teacher implements Serializable {
private String name;
private Person person;
public Teacher(String name, Person person) {
this.name = name;
this.person = person;
}
public static void main(String[] args) throws Exception {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
Person person = new Person("路飛", 20);
Teacher teacher = new Teacher("雷利", person);
oos.writeObject(teacher);
}
}
}
我們看到程序直接報錯,因爲Person類的對象是不可序列化的,這導致了Teacher的對象不可序列化
1.3 同一對象序列化多次的機制
同一對象序列化多次,會將這個對象序列化多次嗎?答案是否定的。
public class WriteTeacher {
public static void main(String[] args) throws Exception {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
Person person = new Person("路飛", 20);
Teacher t1 = new Teacher("雷利", person);
Teacher t2 = new Teacher("紅髮香克斯", person);
//依次將4個對象寫入輸入流
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(person);
oos.writeObject(t2);
}
}
}
依次將t1、t2、person、t2對象序列化到文件teacher.txt文件中。
注意:反序列化的順序與序列化時的順序一致。
public class ReadTeacher {
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) {
Teacher t1 = (Teacher) ois.readObject();
Teacher t2 = (Teacher) ois.readObject();
Person p = (Person) ois.readObject();
Teacher t3 = (Teacher) ois.readObject();
System.out.println(t1 == t2);
System.out.println(t1.getPerson() == p);
System.out.println(t2.getPerson() == p);
System.out.println(t2 == t3);
System.out.println(t1.getPerson() == t2.getPerson());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//輸出結果
//false
//true
//true
//true
//true
從輸出結果可以看出,Java序列化同一對象,並不會將此對象序列化多次得到多個對象。
- Java序列化算法
-
所有保存到磁盤的對象都有一個序列化編碼號
-
當程序試圖序列化一個對象時,會先檢查此對象是否已經序列化過,只有此對象從未(在此虛擬機)被序列化過,纔會將此對象序列化爲字節序列輸出。
-
如果此對象已經序列化過,則直接輸出編號即可。
圖示上述序列化過程。
1.4 java序列化算法潛在的問題
由於java序利化算法不會重複序列化同一個對象,只會記錄已序列化對象的編號。如果序列化一個可變對象(對象內的內容可更改)後,更改了對象內容,再次序列化,並不會再次將此對象轉換爲字節序列,而只是保存序列化編號。
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().equals(p2.getName()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
//輸出結果
//Person{name='9龍', age=23}
//Person{name='海賊王', age=23}
//true
//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}
從輸出我們看到,使用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 版本號,方便項目升級。