談談序列化和反序列化
阿新 • • 發佈:2020-03-16
## 序列化簡介
**Java序列化是指將一個Java物件轉化為一個二進位制流的過程,反序列化是指將二進位制流轉化為一個Java物件的過程**。一般進行序列化的目的有:
- 當程式退出時, 這些物件也就消失了, 而序列化正是為了將這些物件儲存起來以便將來使用;
- 通過網路將序列化後的二進位制流傳輸給遠端`JVM`使用(`RPC`、`RMI`的基礎)。
所有可能在網路上傳輸的物件都應該是可以序列化的,比如`RMI`過程的中引數和返回值;所有需要儲存到磁碟中的物件也應該是可以序列化的,比如說需要儲存到`HttpSession`或者`ServletContext`中的物件。物件要實現序列化,必須實現以下兩個介面的其中一個:
- Serializable
- Externalizable
這邊簡單介紹這兩個介面的區別。
> Externalizable繼承了Serializable,該介面中定義了兩個抽象方法:writeExternal()與readExternal()。當使用Externalizable介面來進行序列化與反序列化的時候需要開發人員重寫writeExternal()與readExternal()方法。還有一點值得注意:在使用Externalizable進行序列化的時候,在讀取物件時,會呼叫被序列化類的無參構造器去建立一個新的物件,然後再將被儲存物件的欄位的值分別填充到新物件中。**所以,實現Externalizable介面的類必須要提供一個public的無參的構造器**。
> 一般當我們序列化和反序列化時要實現些自定義邏輯時,會實現Externalizable介面。
## 物件流
我們要將物件進行序列化,可以使用`ObjectOutputStream`和`ObjectInputStream`進行序列化。
```java
//使用了自動關閉資源的語法糖
try( ObjectOutputStrem oos = new ObjectOutputStrem(new FileOutputStream("object.txt")) ){
Person p = new Person();
oos.writeObject(p);
}catch(Exception e){
//handle Exception
}
```
以上程式碼是將物件序列化到本地檔案中,想要將物件反序列化,可以使用ObjectInputStream。使用方式和上面的程式碼類似。**反序列化時僅僅讀取的是Java物件的資料,讀不到類的資料,因此反序列化時必須要提供這個物件對應的Class檔案,不然會報類找不到異常。另外反序列化時不會通過類的建構函式來初始化類。**
當一個可序列化的類有多個父類時(包括直接父類和間接父類),這些父類要麼有無引數的建構函式,要麼也是可以序列化的,否則這個子類進行反序列化時將丟擲`InvalidClassException`。如果父類是不可以序列化的,只是帶有無引數的建構函式,那麼子類在進行序列化的時候不會將父類中定義的成員變數序列化到二進位制流中。
## 引用型別成員變數的序列化
如果一個類中包含引用變數,那麼只有這個引用變數是可序列化的,這個類本身才是可以序列化的。不然的話無論你是否實現`Serializable`,都是不能進行序列化的。
所有物件只會被序列化一次,不然話會存在這樣一個問題:物件A和B屬於同一個類,他們同時引用了物件C。如果我們序列化A和B時,將物件C序列化兩次的話,那麼在我們反序列化的時候系統中會有兩個C物件。這和我們序列化的初衷不符合。因此Java在序列化的時候採用了下面的序列化演算法:
- 所有儲存到磁碟的物件都會有一個序列化編號;
- 當程式試圖序列化一個物件時,程式會先檢查該物件是否已經被序列化過,只有該物件從未被序列化過(本次JVM中),才會將該物件轉換成位元組序列輸出;
- 如果該物件已經序列化過,程式只會輸出一個序列化編號,不會對該物件再次序列化。
如果是一個可變物件,我們在將其序列化之後,改變物件的內容,然後再試圖對該物件進行序列化,這樣是不會生效的。
## 自定義序列化
當對某個物件進行序列化時,系統會自動把該物件所有例項變數依次進行序列化,如果某個例項引用到另一個物件,則被引用的物件也會被序列化。
如果我們不希望物件的某個屬性被序列化,那麼我們可以在定義這個成員變數時加上`transient`關鍵字。`transient`關鍵字只能修飾例項變數。
`transient`提供的機制過於簡單,如果開發者想對某個例項變數進行比較複雜的序列化機制應該怎麼做呢?在序列化和反序列化的過程中,如果物件需要特殊的處理邏輯,那麼這些物件要提供如下的方法:
```java
//可以通過此方法修改序列化的物件
private Object writeReplace() throws ObjectStreamException;
//方法中呼叫
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
//使用writeObject的預設的序列化方式,除此之外可以加上一些其他的操作,如新增額外的序列化物件到輸出:out.writeObject("XX")
defaultWriteObject()
private void readObject(java.io.ObjectInputStream in) throws Exception;
//可以通過此方法修改返回的物件
private Object readResolve() throws ObjectStreamException;
```
下面給出一個單例序列化和反序列化的列子:
```java
public class PersonSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private PersonSingleton(String name) {
this.name = name;
};
private static PersonSingleton person = null;
public static synchronized PersonSingleton getInstance() {
if (person == null)
return person = new PersonSingleton("cgl");
return person;
}
private Object writeReplace() throws ObjectStreamException {
System.out.println("1 write replace start");
return this;//可修改為其他物件
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("2 write object start");
out.defaultWriteObject(); //out.writeInt(1);
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("3 read object start");
in.defaultReadObject(); //int i=in.readInt();
}
private Object readResolve() throws ObjectStreamException {
System.out.println("4 read resolve start");
return PersonSingleton.getInstance();//不管序列化的操作是什麼,返回的都是本地的單例物件
}
}
```
上面四個方法的呼叫順序依次是writeReplace-->writeObject-->readObject-->readResolve。
另外Java中還提供了另外一種自定義序列化的機制,就是實現`Externalizable`介面,這種機制程式設計師在序列化過程中能有更大的控制權。實現`Externalizable`介面需要實現另外兩個方法`writeExternal`和`readExternal`。此時上面的`writeObject`和`readObject`方法將失效。下面提供一個列子
```java
public class Person implements Externalizable {
private int age;
private String name;
private Person father;
//必須提供預設建構函式
public Person(){
System.out.println("csx-mg");
}
public Person(int age, String name,Person farher) {
this.age = age;
this.name = name;
this.father = farher;
}
//可修改為其他物件
private Object writeReplace() throws ObjectStreamException {
System.out.println("1 write replace start");
return this;
}
//實現Externalizable時,這個方法會失效,不會被呼叫
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("2 write object start");
out.defaultWriteObject();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("csx-mg");
}
//實現Externalizable時,這個方法會失效,不會被呼叫
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("3 read object start");
in.defaultReadObject(); //int i=in.readInt();
}
private Object readResolve() throws ObjectStreamException {
System.out.println("4 read resolve start");
return PersonSingleton.getInstance();//不管序列化的操作是什麼,返回的都是本地的單例物件
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("csx-mg");
}
}
```
以上方法的呼叫順序是writeReplace-->writeExternal-->readExternal-->readResolve
## serialVersionUID 作用
JVM如何判斷序列化與反序列化的類檔案是否相同呢?
並不是說兩個類檔案要完全一樣, 而是通過類的一個私有屬性serialVersionUID來判斷的, 如果我們沒有顯示的指定這個屬性, 那麼JVM會自動使用該類的hashcode值來設定這個屬性, 這個時候如果我們對類進行改變(比如說加一個屬性或者刪掉一個屬性)就會導致serialVersionUID不同, 所以對於準備序列化的類, 一般情況下我們都會顯示的設定這個屬性, 這樣及時以後我們對該類進行了某些改動, 只要這個值保持一樣, JVM就還是會認為這個類檔案是沒有變的。
## 參考
- [關於 Java 物件序列化您不知道的 5 件事](https://www.ibm.com/developerworks/cn/java/j-5things1/)(序列化並不