什么是序列化?
一句話概括。序列化將對象的狀態信息轉換為可以存儲或傳輸的形式的過程。而Java序列化,就是指把Java對象轉換為“有序”字節序列的過程。相反的,把“有序”字節序列恢復為Java對象的過程稱之為反序列化。
怎么理解上面的描述呢?從序列化的定義的可以看出,序列化其實是一個用來保障存儲和傳輸的機制,而其針對的對象,肯定就是那些不便存儲和傳輸的信息,而這體現在java編程中,就是類的實例 —— 類對象。
上面提到的“有序”,它並不是真的有順序,其實是比較抽象化的表達,以此來表達經過序列化處理的對象,能夠通過反序列化恢復成原來的樣子這樣一個過程
舉個例子,你要搬家了,家里有個大鞋架,因為鞋架太大了,不便存儲和運輸,於是你把鞋機拆散了,然后把拆解步驟記錄下來,運輸到新家后,你再根據步驟記錄把零件組裝成鞋架原來的樣子。拆鞋架和組裝鞋架其實就是序列化和反序列的過程,而你記錄的那份拆解步驟,就是序列化協議。
怎么實現序列化?
一、實現Serializable接口
定義一個類實現Serializable接口:
public class Rectangle implements Serializable {
private int width;
private int length;
public Rectangle(){}
public Rectangle(int width,int length){
this.width = width;
this.length = length;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", length=" + length +
'}';
}
}
二、實現Externalizable接口
實現
writeExternal、readExternal方法
public class Rectangle implements Externalizable {
private int width;
private int length;
public Rectangle(){}
public Rectangle(int width,int length){
this.width = width;
this.length = length;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", length=" + length +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
序列化和反序列化
public static void main(String[] args){ /* 序列化 */ Rectangle rectangle = new Rectangle(6,8); try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rectangle.out"))){ oos.writeObject(rectangle); }catch(Exception ex){ ex.printStackTrace(); } /* 反序列化 */ try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("rectangle.out"))){ Rectangle rectangle1 = (Rectangle) ois.readObject(); System.out.println(rectangle1); }catch(Exception ex){ ex.printStackTrace(); } }正常的輸出結果:
Rectangle{width=6, length=8}當實現Externalizable接口,沒有對
writeExternal和readExternal重寫時,結果如下
Rectangle{width=0, length=0}也就是說的,實現Externalizable接口,需要自己實現序列化和反序列邏輯,編程人員用比較高的靈活度,這點在接下來的自定義序列化會講到
自定義序列化?
transient關鍵字
由transient關鍵字修飾的成員的變量,序列化時將被忽略。該方法決定變量是否序列化。
該自定義方法級別最低,當使用的下面的任一方法時,
transient關鍵字將失效。換句話說,使用下面各方法進行序列化自定義,那么transient修飾的變量同樣會被序列化。
自定義readObject和writeObject方法
通過的類中自定義readObject和writeObject方法,可以控制該類的序列化和反序列化邏輯,jvm在進行序列化的時候會自動調用readObject方法,反序列化調用writeObject。
private void readObject(ObjectInputStream ois) throws IOException {
this.length = ois.readInt() / 100;
this.width = ois.readInt();
}
private void writeObject(ObjectOutputStream oos)throws IOException{
this.length = this.length * 100;
oos.writeInt(this.length);
oos.writeInt(this.width);
}
自定義writeReplace方法
writeReplace方法沒有入參,用於改變序列化對象的類型,且會使用默認的序列化機。也就是說,上面提到的自定義readObject和writeObject方法都將失效。以下為實例:
private Object writeReplace() throws ObjectStreamException {
List<Object> list = new ArrayList<>();
list.add(this.length * 1000);
list.add(this.width *1000);
return list;
}
相應的,在進行反序列化的時候,就應該使用新的類型進行接收,如該例子,應該用
List<Object>接收反序列化對象。
自定義readResolve方法
readResolve方法用於替換反序列化出來的對象,該方法同樣沒有入參。
private Object readResolve() throws ObjectStreamException {
System.out.println("自定義readResolve方法被調用");
return new Rectangle(9,10);
}
由於沒發拿到序列化的信息,因此常用來控制單例模式下,反序列化出來的對象為原先的單例對象,以維護單例模式中反序列化對象的單一性。
實現Externalizable接口
這個已經在上面實例化方式中有所提及。實現該接口必須強制實現writeExternal和readExternal 兩個方法如下:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
this.length = this.length * 100;
out.writeInt(this.length);
out.writeInt(this.width);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.length = in.readInt() / 100;
this.width = in.readInt();
}
該效果等同於上面實現Serializable接口,然后自定義readObject和writeObjectf方法
序列化的注意事項
-
serialVersionUID 序列化版本號
用來區分需要序列化類的版本。假如序列化的類中發生了變量的修改,那么該版本號一般也要跟着修改,才能夠在反序列化的時候得到對應版本的類對象,否則會拋出
InvalidClassException異常 -
序列化與類的初始化一樣,是的一個遞歸的過程。
當一個類實現了序列化接口,該類的成員除了基本類型和String類型之外,其他的引用類型也必須是可序列化的;否則會拋
NotSerializableException異常 -
同一對象多次序列化只會序列化一次
如果程序對同一個對象有多次序列化,那么只會在第一次進行序列化,后續實際上是返回一個編號進行區分
-
反序列化的順序與序列化時的順序一致,類似隊列模型
序列化的使用場景
講了這么多,那序列化到底用在什么地方呢?
由jvm的內存結構相關知識我們可以知道,java對象都被保存在堆內存中。在jvm處於運行狀態的時候,我們能夠對對象的進行復用。但一旦jvm生命周期結束,相關對象也隨之被回收。也就是說的,如果希望在jvm停止后還能夠拿到某些對象,這時候就需要用到java的序列化。
因此大概由以下幾種場景會使用到java的序列化
- 當你想把的內存中的對象狀態保存到一個文件中或者數據庫中時候;
- 當你想用套接字在網絡上傳送對象的時候;
- 當你想通過RMI(遠程方法調用)傳輸對象的時候;
