面試官: 為什麼不能輕易修改 serialVersionUID 字段?

阿里巴巴開發手冊,(四)OOP 規約,第 13 條解釋如下:

【強制】序列化類新增屬性時,請不要修改 serialVersionUID 字段,避免反序列失敗;如果 完全不兼容升級,避免反序列化混亂,那麼請修改 serialVersionUID 值。說明:注意 serialVersionUID 不一致會拋出序列化運行時異常。

首先需要解釋一下這條規則,並不是要求你一定不可以修改,而是根據自己的需要來修改。我們先了解一下 serialVersionUID 是幹嘛的。

序列化

首先我們需要了解一下序列化,我們可以簡單了理解序列化就是把 Java 對象轉換成另一種形態的數據,這種形態的數據可以用於存儲或者是傳輸。因為本身 Java 對象是存在內存中,沒有辦法直接存儲或者是傳輸,序列化的出現來解決這個問題,那麼反序列化就是把形態數據重新轉換為 Java 對象。當然這個形態可以是多種格式,比如我們詳知的 JSON,或者是 Byte,也可以是我們的自定義形式,比如 K-V 形式,如下圖。

面試官: 為什麼不能輕易修改 serialVersionUID 字段?

Java 默認序列化

說到 serialVersionUID 就不得不說到 Java 默認的序列化,為了提供上文我們說的存儲和傳出,Java 默認提供了一種序列化方式。只要序列化的類實現 java.io.Serializable接口,這樣就可以做序列化和反序列化了。我簡單羅列了一下代碼這樣更直觀(有所刪減)。

User.java

<code>public class User implements java.io.Serializable {
private String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}/<code>

SerializerTest.java

<code>User user = new User("碼匠筆記");
FileOutputStream fo = new FileOutputStream("user.bytes");
ObjectOutputStream so = new ObjectOutputStream(fo);
so.writeObject(user);
FileInputStream fi = new FileInputStream("user.bytes");
ObjectInputStream si = new ObjectInputStream(fi);
user = (User) si.readObject();/<code>

代碼已經比較直觀,User 實現了 Serializable 接口,通過 ObjectOutputStream 把類轉化為字節碼存入 user.bytes,然後再使用 ObjectInputStream 把字節碼從 user.bytes 讀入內存。

面試官: 為什麼不能輕易修改 serialVersionUID 字段?

這樣非常簡單就實現了 Java 的序列化,說了這麼多它真的有用途嗎?當然,比如Java自帶的遠程調用組件 RMI。

serialVersionUID

終於說到重點了,為什麼不能輕易修改 serialVersionUID?可是上面的代碼中我們明明就沒有設置 serialVersionUID。那我們調整一下例子,再測試一次。User.java

<code>public class User implements java.io.Serializable {
private String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}/<code>

UserSerializeTest.java

<code>public class UserSerializeTest {
public static void main(String[] args) throws Exception {
User user = new User("碼匠筆記");
FileOutputStream fo = new FileOutputStream("user.bytes");
ObjectOutputStream so = new ObjectOutputStream(fo);
so.writeObject(user);
so.close();
}
}/<code>

User.java

<code>public class User implements java.io.Serializable {
private String name;
private String desc;
public User(String name) {
this.name = name;
}
@Override
public String toString() {

return name;
}
}/<code>

UserDeserializeTest.java

<code>public class UserDeserializeTest {
public static void main(String[] args) throws Exception {
FileInputStream fi = new FileInputStream("user.bytes");
ObjectInputStream si = new ObjectInputStream(fi);
User user = (User) si.readObject();
System.out.println(user);
si.close();
}
}/<code>

如上代碼,我們先運行 UserSerializeTest.java,然後修改 User.java,添加一個屬性 desc,然後再次運行代碼。果然出錯了?

<code>Exception in thread "main" java.io.InvalidClassException:
com.github.codedrinker.p1413.User;
local class incompatible:
stream classdesc serialVersionUID = 6360520658036414457,
local class serialVersionUID = -3025746955499933156/<code>

顯示 serialVersionUID 不相同,反序列化失敗了,可是我們沒有定義 serialVersionUID是為什麼呢?(所有源碼文末均有獲取方式)是時候讓源碼君出面了,我們查看 java.io.ObjectStreamClass#writeNonProxy ,如果當前類(User)沒有定義 serialVersionUID,就會調用java.io.ObjectStreamClass#computeDefaultSUID生成默認的序列化唯一標示。我們簡單的看代碼,發現他的生成規則是根據類名,結果明,方法和屬性等參數生成的 hash 值,所以我們給 User 添加了 desc 屬性,所以對應的 serialVersionUID 肯定會變化。

面試官: 為什麼不能輕易修改 serialVersionUID 字段?

JVM 規範[1] 裡面也有具體的解釋

<code>The stream-unique identifier is
a 64-bit hash of the class name,
interface class names,
methods, and fields./<code>

這裡我們也可以使用 JDK 自帶的工具 serialver 來驗證一下,這個工具和 JDK 默認的生成 serialVersionUID 的規則一樣。

<code>serialver com.github.codedrinker.p1413.User/<code>

可以得到添加 desc 前後的 serialVersionUID ,輸出如下

<code> // 未添加 desc
private static final long serialVersionUID
= 6360520658036414457L;
// 添加 desc
private static final long serialVersionUID
= -3025746955499933156L;/<code>

好了,所以看到這裡我們修改一個地方就可以解決這個問題了。手工定義一個 serialVersionUID 代碼如下User.java

<code>public class User implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}/<code>

這樣再重複上面的運行步驟,就可以成功的做反序列化了。

面試官: 為什麼不能輕易修改 serialVersionUID 字段?

到這裡我們就全部明白了為什麼文檔裡面說明不能輕易的修改 serialVersionUID 了。但是每次定義成 1L 也不是辦法,所以可以配置一下 IDEA,這樣就可以創建類的時候提示自動生成了。

面試官: 為什麼不能輕易修改 serialVersionUID 字段?

面試官: 為什麼不能輕易修改 serialVersionUID 字段?

其他序列化方式

手冊裡面只提到了 Java 默認的序列化方式,其實還有很多性能很不錯的序列化方式,正如上文中提到的 JSON 或是字節流,比如現在比較流行的幾種:Hessian、Kryo、Fastjson、ProtoBuf、Jackson 等,具體在這裡就不展開了,他們都有自己的優缺點,下面是 jvm-serializers[2] 輸出的它們的性能比較,可以在選擇的時候有限參考下。

面試官: 為什麼不能輕易修改 serialVersionUID 字段?



分享到:


相關文章: