序列化是一種Java對象持久化的手段,普遍應用在網絡傳輸、RMI等場景中,這篇文章將深入講解 Java 的序列化的原理以及如何自定義序列化策略。 |
Java 對象的序列化原理
☕️ 爲什麼要序列化
一般情況下,我們在 Java 平臺創建的對象只有在 JVM 運行時纔會存在,即這些對象的生命週期不會比 JVM 的生命週期更長。但在現實應用中,可能需要在 JVM 停止運行之後能夠保存(持久化)指定的對象,並在將來重新讀取被保存的對象。所以,爲了實現這樣的需求就有了我們的序列化功能。
使用 Java 對象序列化保存對象時,會把對象的狀態保存爲一組字節,在未來,再將這些字節組裝成對象。必須注意地是:對象序列化保存的是對象的”狀態”,即它的成員變量,而不會保存類中的靜態變量。
除了在持久化對象時會用到對象序列化之外,當使用 RMI(Romote Method Invocation,遠程方法調用),或在網絡中傳遞對象時,都會用到對象序列化,並且 Java 序列化 API 爲處理對象序列化提供了一個標準機制,該API簡單易用。
☕️ Java對象的序列化和反序列方式
在Java中,只要一個類實現了 java.io.Serializable 接口,那麼它就可以被序列化。我們以一個 demo 代碼爲例:
創建一個用於序列化的 Person 類
public class Person implements Serializable {
private String name;
private transient int age;
private static final long serialVersionUID = -6849794470754667710L;
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
@Override
public String toString() {
return "Person { " +
"name = '" + this.name + "', " +
"age = " + this.age + " }";
}
}
對 Person 進行序列化和反序列化
public static void main(String[] args) {
// 創建一個待序列化的對象
Person person = new Person();
person.setName("Andy");
person.setAge(24);
System.out.println(person);
// 把對象序列化到文件中去
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person"));
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
// 從文件中反序列化成對象
File file = new File("person");
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person"));
Person newPerson = (Person) ois.readObject();
System.out.println(newPerson);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
>>>>>
Person { name = 'Andy', age = 24 }
Person { name = 'Andy', age = 0 }
現在我們來介紹一下對象序列化和反序列化的相關重要知識:
- 一個類只有實現了 java.io.Serialzable 接口纔可以被序列化。
- 通過 ObjectOutputStream 和 ObjectInputStream 來進行對象的序列化和反序列化。
- 虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,而且還取決於兩個類的序列化ID(private static final long serialVersionUID)是否一致。
- 序列化不保存靜態變量,因爲序列化保存的是對象的狀態,靜態變量屬於類的狀態,因此序列化並不保存靜態變量。
- 要想將父類對象也序列化,就需要讓父類也實現 Serializable 接口。
- Transient 關鍵字的作用是控制變量的序列化:阻止該變量被序列化到文件中。在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。
提示:服務器端給客戶端發送序列化對象數據時,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,纔可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。
☕️ 序列化存儲規則
Java 序列化機制爲了節省磁盤空間,具有特定的存儲規則,當寫入文件的爲同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用和一些控制信息的空間,反序列化時,恢復引用關係,所以寫入同一個對象多次後反序列化得到的對象是指向同一個對象的引用。該存儲規則極大的節省了存儲空間。
比如下面的代碼例子:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("result.obj"));
Test test = new Test();
test.num = 1;
oos.writeObject(test);
oos.flush();
test.num = 2;
oos.writeObject(test);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("result.obj"));
Test t1 = (Test) ois.readObject();
Test t2 = (Test) ois.readObject();
System.out.println("t1.num = " + t1.num);
System.out.println("t2.num = " + t2.num);
>>>>>
t1.num = 1
t2.num = 1
本案例的目的是希望將 test 對象兩次保存到 result.obj 文件中,寫入一次以後修改對象屬性值再次保存第二次,然後從 result.obj 中再依次讀出兩個對象,輸出這兩個對象的 num 屬性值。案例代碼的目的原本是希望一次性傳輸對象修改前後的狀態。
結果兩個輸出的都是 1, 原因就是第一次寫入對象以後,第二次再試圖寫的時候,虛擬機根據引用關係知道已經有一個相同對象已經寫入文件,因此只保存第二次寫的引用,所以讀取時,都是第一次保存的對象。因此在使用同一個文件多次 writeObject 時,需要特別注意這個問題。
自定義序列化和反序列化方式
我們通過 ArrayList 來了解一下如何進行自定義序列化和反序列化。我們先寫一個關於 ArrayList 序列化和反序列化的例子吧:
public static void main(String[] args) {
// 創建待序列化ArrayList對象
List<String> strList = new ArrayList<>();
strList.add("Alpha");
strList.add("Beta");
strList.add("Gamma");
System.out.println("origin list: " + strList);
// 序列化ArrayList對象
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("stringList"));
oos.writeObject(strList);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化得到新的ArrayList對象
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("stringList"));
List<String> newStrList = (ArrayList<String>)ois.readObject();
System.out.println("new list: " + newStrList);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
>>>>>
origin list: [Alpha, Beta, Gamma]
new list: [Alpha, Beta, Gamma]
代碼的結果和我們預期的一致,好,那麼我們去看一下 ArrayList 的源碼:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
transient Object[] elementData; // non-private to simplify nested class access
private int size;
}
從源碼我們可以看出,ArrayList 確實是實現了 Serializable 接口,這就說明可以對它進行序列化及反序列化操作。但存放數據的 elementData 數組是 transient 的,那麼在序列化過程中,本應該數據不會被保存下來纔對的,但爲什麼保留下來了呢?答案就是:ArrayList 定義了來個方法: writeObject 和 readObject
在介紹 writeObject 和 readObject 之前我們需要先了解一下重要的知識點:
- 在序列化過程中,如果被序列化的類中定義了 writeObject 和 readObject 方法,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化
* 如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法 - 用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值
好了,現在我們進去看一下這兩個方法裏面的具體實現源碼:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioral compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
那麼爲什麼 ArrayList 要用這種方式來實現序列化呢?
我們知道,ArrayList 實際上是動態數組,每次在放滿以後自動增長設定的長度值,如果數組自動增長長度設爲 100,而實際只放了一個元素,那就會序列化 99 個 null 元素。所以,爲了優化存儲以及在序列化的時候不會將這麼多 null 同時進行序列化,ArrayList 把元素數組設置爲了 transient,並通過重寫 writeObject 和 readObject 方法的方式把其中的元素保留下來
現在我們來分析一下,重寫的 writeObject 和 readObject 是如何被調用的:
對象的序列化過程通過 ObjectOutputStream 和 ObjectInputputStream 來實現的,我們以 ObjectOutputStream 爲例,ObjectOutputStream 序列化時使用的是 writeObject 方法,那好,我們就進去源碼看一下,限於篇幅,這裏我一步步跟蹤得到 writeObject 方法的調用棧爲:
writeObject >> writeObjecto >> writeOrdinaryObject >> writeSerialData >> invokeWriteObject |
我們來看一下 invokeWriteObject 的源碼:
void invokeWriteObject(Object obj, ObjectOutputStream out)
throws IOException, UnsupportedOperationException{
if (writeObjectMethod != null) {
try {
writeObjectMethod.invoke(obj, new Object[]{ out });
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
其中 writeObjectMethod.invoke(obj, new Object[]{ out }) 是關鍵,通過反射的方式調用 writeObjectMethod 方法,官方是這麼解釋這個 writeObjectMethod 的:
class-defined writeObject method, or null if none |
所以,一個類的 writeObject 和 readObject 方法,在使用 ObjectOutputStream 的 writeObject 方法和 ObjectInputStream 的 readObject 方法時,會通過反射的方式調用。
📖 總結:
- 如果一個類想被序列化,需要實現Serializable接口。否則將拋出NotSerializableException異常,這是因爲,在序列化操作過程中會對類型進行檢查,要求被序列化的類必須屬於Enum、Array和Serializable類型其中的任何一種
- 在變量聲明前加上 transient 關鍵字,可以阻止該變量被序列化到文件中
- 在類中增加writeObject 和 readObject 方法可以實現自定義序列化方式