背景:
最近我們在做一個智能的對話機器人,是垂直領域的,然后會分析用戶的模型,在這個過程中會問用戶一些問題,之前用戶沒回答一次,我們就從DB中去把這個用戶的session給取出來,但是當以后用戶量很大的時候,這無疑給DB造成了很大的壓力,所以,我就加了一層緩存用的redis,因為要把對象序列化到redis,所以PO要實現那個Serializable接口,然后給一個UID,所以就想深入研究一下,這個serialVersionUID 有什么作用。
問題:
當一個對象實現 Serializable 接口時,多數 ide 會提示聲明一個靜態常量 serialVersionUID(版本標識),那 serialVersionUID 到底有什么作用呢?應該如何使用 serialVersionUID ?
探索歷程:
serialVersionUID 是實現 Serializable 接口而來的,而 Serializable 則是應用于Java 對象序列化/反序列化。對象的序列化主要有兩種用途:
把對象序列化成字節碼,保存到指定介質上(如磁盤等)
用于網絡傳輸
現在反過來說就是,serialVersionUID 會影響到上述所提到的兩種行為。那到底會造成什么影響呢?
java.io.Serializable doc 文檔,給出了一個相對詳細解釋:
serialVersionUID 是 Java 為每個序列化類產生的版本標識,可用來保證在反序列時,發送方發送的和接受方接收的是可兼容的對象。如果接收方接收的類的 serialVersionUID 與發送方發送的 serialVersionUID 不一致,進行反序列時會拋出 InvalidClassException。序列化的類可顯式聲明 serialVersionUID 的值,如下:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
當顯式定義 serialVersionUID 的值時,Java 根據類的多個方面(具體可參考 Java 序列化規范)動態生成一個默認的 serialVersionUID 。盡管這樣,還是建議你在每一個序列化的類中顯式指定 serialVersionUID 的值,因為不同的 jdk 編譯很可能會生成不同的 serialVersionUID 默認值,進而導致在反序列化時拋出 InvalidClassExceptions 異常。所以,為了保證在不同的 jdk 編譯實現中,其 serialVersionUID 的值也一致,可序列化的類必須顯式指定 serialVersionUID 的值。另外,serialVersionUID 的修飾符最好是 private,因為 serialVersionUID 不能被繼承,所以建議使用 private 修飾 serialVersionUID。
舉例說明如下: 現在嘗試通過將一個類 Person 序列化到磁盤和反序列化來說明 serialVersionUID 的作用: Person 類如下:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
public Person() {
}
public Person(String name, Integer age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
簡單的測試一下:
@Test
public void testversion1L() throws Exception {
File file = new File("person.out");
// 序列化
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
Person person = new Person("Haozi", 22, "上海");
oout.writeObject(person);
oout.close();
// 反序列化
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newPerson = oin.readObject();
oin.close();
System.out.println(newPerson);
}
測試發現沒有什么問題。有一天,因發展需要, 需要在 Person 中增加了一個字段 email,如下:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
private String email;
public Person() {
}
public Person(String name, Integer age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
public Person(String name, Integer age, String address,String email) {
this.name = name;
this.age = age;
this.address = address;
this.email = email;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
", email='" + email + '\'' +
'}';
}
}
這時我們假設和之前序列化到磁盤的 Person 類是兼容的,便不修改版本標識 serialVersionUID。再次測試如下:
@Test
public void testversion1LWithExtraEmail() throws Exception {
File file = new File("person.out");
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newPerson = oin.readObject();
oin.close();
System.out.println(newPerson);
}
將以前序列化到磁盤的舊 Person 反序列化到新 Person 類時,沒有任何問題。
可當我們增加 email 字段后,不作向后兼容。即放棄原來序列化到磁盤的 Person 類,這時我們可以將版本標識提高,如下:
private static final long serialVersionUID = 2L;
再次進行反序列化,則會報錯,如下:
java.io.InvalidClassException:Person local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
談到這里,我們大概可以清楚,serialVersionUID 就是控制版本是否兼容的,若我們認為修改的 Person 是向后兼容的,則不修改 serialVersionUID;反之,則提高 serialVersionUID的值。再回到一開始的問題,為什么 ide 會提示聲明 serialVersionUID 的值呢?
因為若不顯式定義 serialVersionUID 的值,Java 會根據類細節自動生成 serialVersionUID 的值,如果對類的源代碼作了修改,再重新編譯,新生成的類文件的serialVersionUID的取值有可能也會發生變化。類的serialVersionUID的默認值完全依賴于Java編譯器的實現,對于同一個類,用不同的Java編譯器編譯,也有可能會導致不同的serialVersionUID。所以 ide 才會提示聲明 serialVersionUID 的值。