序列化和反序列化的概念
1、把對象轉換爲字節序列的過程稱爲對象的序列化。
2、把字節序列恢復爲對象的過程稱爲對象的反序列化。
對象的序列化主要有兩種用途:
1) 把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中;
2) 在網絡上傳送對象的字節序列。
當兩個進程在進行遠程通信時,彼此可以發送各種類型的數據。無論是何種類型的數據,都會以二進制序列的形式在網絡上傳送。發送方需要把這個Java對象轉換爲字節序列,才能在網絡上傳送;接收方則需要把字節序列再恢復爲Java對象。
舉個真實的案例:當我們需要使用的對象很複雜或者需要很長時間去構造,這時就會引入使用代理模式(Proxy)。例如:如果構建一個對象很耗費時間和計算機資源,代理模式(Proxy)允許我們控制這種情況,直到我們需要使用實際的對象。一個代理(Proxy)通常包含和將要使用的對象同樣的方法,一旦開始使用這個對象,這些方法將通過代理(Proxy)傳遞給實際的對象。
解讀:在微服務化盛行的今天,很多複雜的對象構造起來比較耗時,爲了節省開支,某些公司將這部分複雜的對象先圈起來,寫成服務起在遠端B,並在調用端A端以代理(Proxy)的形式提供對服務的訪問,這期間從B到A遠程調的過程形成了Java對象序列化和反序列化的相關操作!
jdk類庫中序列化api
java.io.ObjectOutputStream
代表對象輸出流,它的writeObject(Object obj)方法可對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中。
java.io.ObjectInputStream
代表對象輸入流,它的readObject()方法從一個源輸入流中讀取字節序列,再把它們反序列化爲一個對象,並將其返回。
只有實現了Serializable和Externalizable接口的類的對象才能被序列化。Externalizable接口繼承自 Serializable接口,實現Externalizable接口的類完全由自身來控制序列化的行爲,而僅實現Serializable接口的類可以 採用默認的序列化方式
對象序列化包括如下步驟:
1) 創建一個對象輸出流,它可以包裝一個其他類型的目標輸出流,如文件輸出流;
2) 通過對象輸出流的writeObject()方法寫對象。
對象反序列化的步驟如下:
1) 創建一個對象輸入流,它可以包裝一個其他類型的源輸入流,如文件輸入流;
2) 通過對象輸入流的readObject()方法讀取對象。
java代碼實現:
package com.awakeyo.serializable;
import java.io.Serializable;
/**
* @author awakeyoyoyo
* @className Person
* @description TODO
* @date 2020-03-31 13:53
*/
public class Person implements Serializable {
private static final long serialVersionUID=13316311153L;
private int age;
private String name;
private String sex;
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
'}';
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
package com.awakeyo.serializable;
import com.sun.org.apache.bcel.internal.generic.NEW;
import java.io.*;
/**
* @author awakeyoyoyo
* @className SerializePersonTest
* @description TODO
* @date 2020-03-31 13:57
*/
public class SerializePersonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person=new Person();
person.setName("lqhao");
person.setAge(21);
person.setSex("男");
ObjectOutputStream oo=new ObjectOutputStream(new FileOutputStream(new File("./person.txt")));
oo.writeObject(person);
System.out.println("Person對象序列化成果");
oo.close();
System.out.println("--------------------");
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("./person.txt"));
Person person1=(Person) ois.readObject();
System.out.println("Persion反序列化成果");
System.out.println(person1.toString());
ois.close();
}
}
運行結果
serialVersionUID的作用
serialVersionUID適用於Java的序列化機制。簡單來說,Java的序列化機制是通過判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認爲是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常,即是InvalidCastException。
具體的序列化過程是這樣的:序列化操作的時候系統會把當前類的serialVersionUID寫入到序列化文件中,當反序列化時系統會去檢測文件中的serialVersionUID,判斷它是否與當前類的serialVersionUID一致,如果一致就說明序列化類的版本與當前類版本是一樣的,可以反序列化成功,否則失敗。
serialVersionUID有兩種顯示的生成方式:
一是默認的1L,比如:private static final long serialVersionUID = 1L;
二是根據類名、接口名、成員方法及屬性等來生成一個64位的哈希字段,比如:
private static final long serialVersionUID = xxxxL;
當實現java.io.Serializable接口的類沒有顯式地定義一個serialVersionUID變量時候,Java序列化機制會根據編譯的Class自動生成一個serialVersionUID作序列化版本比較用,這種情況下,如果Class文件(類名,方法明等)沒有發生變化(增加空格,換行,增加註釋等等),就算再編譯多次,serialVersionUID也不會變化的
靜態變量序列化
修改代碼
package com.awakeyo.serializable;
import java.io.Serializable;
/**
* @author awakeyoyoyo
* @className Person
* @description TODO
* @date 2020-03-31 13:53
*/
public class Person implements Serializable {
private static final long serialVersionUID=13316311153L;
private int age;
private String name;
private String sex;
public static int staticVar = 10;
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
'}';
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
package com.awakeyo.serializable;
import com.sun.org.apache.bcel.internal.generic.NEW;
import java.io.*;
/**
* @author awakeyoyoyo
* @className SerializePersonTest
* @description TODO
* @date 2020-03-31 13:57
*/
public class SerializePersonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person=new Person();
person.setName("lqhao");
person.setAge(21);
person.setSex("男");
ObjectOutputStream oo=new ObjectOutputStream(new FileOutputStream(new File("./person.txt")));
oo.writeObject(person);
System.out.println("Person對象序列化成果");
oo.close();
System.out.println("--------------------");
Person.staticVar=999;
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("./person.txt"));
Person person1=(Person) ois.readObject();
System.out.println("Persion反序列化成果");
System.out.println(person1.toString());
System.out.println("staticvar="+person1.staticVar);
ois.close();
}
}
最終結果
最後的輸出是 999,打印的 staticVar 是從讀取的對象裏獲得的,應該是保存時的狀態纔對。之所以打印 999的原因在於序列化時,並不保存靜態變量,這其實比較容易理解,序列化保存的是對象的狀態,靜態變量屬於類的狀態,因此 序列化並不保存靜態變量。
父類的序列化與 Transient 關鍵字
父類的序列化
要想將父類對象也序列化,就需要讓父類也實現Serializable 接口。如果父類不實現的話的,就 需要有默認的無參的構造函數。在父類沒有實現 Serializable 接口時,虛擬機是不會序列化父對象的,而一個 Java 對象的構造必須先有父對象,纔有子對象,反序列化也不例外。所以反序列化時,爲了構造父對象,只能調用父類的無參構造函數作爲默認的父對象。因此當我們取父對象的變量值時,它的值是調用父類無參構造函數後的值。如果你考慮到這種序列化的情況,在父類無參構造函數中對變量進行初始化,否則的話,父類變量值都是默認聲明的值,如 int 型的默認是 0,string 型的默認是 null。
Transient 關鍵字
作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。