序列化與serialVersionUID

序列化與serialVersionUID


存儲一個對象時,對象所屬類的描述信息也必須存儲。類的描述信息包括:java

  1. 類名
  2. 序列化版本ID(serialVersionUID)
  3. 描述序列化方法的標誌集
  4. 對數據域的描述

serialVersionUID至關於類的「指紋」。serialVersionUID是經過對類、超類、接口、域類型和方法簽名按照規範方式排序,而後將安全散列算法(SHA)應用於這些數據而得到的。算法

SHA是一種能夠爲較大的信息塊提供「指紋」的高效算法,不管數據庫尺寸有多大,生成的「指紋」總之20個字節的數據包。它是經過在數據上執行一個靈巧的位操做序列而建立的,這個序列在本質上能夠保證不管這些數據以何種方式發生改變,其指紋100%會跟着發生改變。序列化機制只使用了SHA碼的前8個字節做爲類的「指紋」,即使如此,當類的數據域或方法發生變化時,其「指紋」跟着發生改變的可能性仍是很是大。數據庫

在反序列化一個對象時,會拿保存的類指紋與類當前的指紋進行比對,若是它們不匹配,說明這個類的定義在該對象被序列化之後發生過改變,所以會產生一個異常。安全


重現異常

有Employee類:測試

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
    private String name;
    private Integer age;
    private char sex;
    private Double salary;
}

執行下面的代碼:ui

public static void main(String[] args) throws Exception {
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("employee.dat"));
    Employee employee = new Employee("xzy", 22, 'm', 100000.0);
    outputStream.writeObject(employee);
    
    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
    Employee employee1 = (Employee) inputStream.readObject();
}

代碼順利執行完成,控制檯打印出以下信息:code

Employee(name=xzy, age=22, sex=m, salary=100000.0)

若先將對象存儲:對象

ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("employee.dat"));
Employee employee = new Employee("xzy", 22, 'm', 100000.0);
outputStream.writeObject(employee);

而後對Employee類進行略微的修改:將成員變量salary的名字改成「salary_」。排序

private Double salary_;//salary → lalary_

最後嘗試反序列化對象:接口

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
Employee employee1 =  (Employee) inputStream.readObject();
System.out.println(employee1);

上述代碼在執行過程當中拋出異常,異常信息以下所示:

Exception in thread "main" java.io.InvalidClassException: com.learn.java.extend.Employee; local class incompatible: stream classdesc serialVersionUID = -7427550135122105667, local class serialVersionUID = 5815872246558374312

從異常信息能夠看到,對象序列化的時候,Employee類的指紋爲-7427550135122105667,反序列化時,Employee類的指紋已經變爲了5815872246558374312,兩者不匹配,所以拋出異常。


解決異常

類的修改很難避免,但類能夠代表本身對早期版本保持兼容。

上述代碼運行產生的異常能夠這樣解決:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
    public static final long serialVersionUID = -7427550135122105667L;
    
    private String name;
    private Integer age;
    private char sex;
    private Double salary_;//salary → lalary_
}

能夠看到,Employee類中添加了一個名爲serialVersionUID的靜態成員變量。若是你觀察的再仔細一點還能發現,該變量保存的值就是上文異常信息中,對象序列化時Employee類的「指紋」。先運行一下代碼,看看異常解決沒有:

控制檯輸出信息:

Employee(name=xzy, age=22, sex=m, salary_=null)

從結果來看,問題確實已經解決了,至少再也不拋出異常了。


初步理解

「若是一個類具備名爲serialVersionUID的靜態數據成員,它就不在須要計算指紋,只需直接使用這個值。一旦這個靜態數據成員被置於某個類的內部,那麼序列化系統就能夠讀入這個類的不一樣版本的對象。」 ——《Java核心技術》

上面這段話,我試着理解了一下:若是類中具備名爲serialVersionUID的靜態成員變量,類就不須要使用SHA計算「指紋」,而是直接將這個值做爲指紋。所以,不管類發生怎樣的修改,只要serialVersionUID不改變,類的「指紋」就不改變,因此對任意版本的對象進行反序列均可以。

我將嘗試用下面3個測試驗證一下個人理解:

1. 爲Employee類添加serialVersionUID,序列化一個對象,修改Employee類,反序列化該對象。

    預期結果:反序列化成功。由於serialVersionUID沒有改變。

2. 爲Employee類添加serialVersionUID,序列化一個對象,修改serialVersionUID,反序列化該對象。

    預期結果:反序列化失敗。由於serialVersionUID發生改變。

3. 爲Employee類添加serialVersionUID,序列化一個對象,爲其餘類添加相同的serialVersionUID,將對象反序列化爲其餘對象。

    預期結果:類型轉換錯誤。

Employee類:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
    private static final long serialVersionUID = 5815872246558374312L;
    private String name;
    private Integer age;
    private char sex;
    private Double salary;
}

  1. 爲Employee類添加serialVersionUID,序列化一個對象,修改Employee類,反序列化該對象。

序列化對象:

ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("employee.dat"));
Employee employee = new Employee("xzy", 22, 'm', 100000.0);
outputStream.writeObject(employee);

修改Employee類:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
    private static final long serialVersionUID = 5815872246558374312L;
    private String name;
    private Integer age;
    private char sex;
    private Double salary;
    private String address;//新添加
}

反序列化該對象:

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
Employee employee1 = (Employee) inputStream.readObject();
System.out.println(employee1);

程序執行正常,控制檯信息以下:

Employee(name=xzy, age=22, sex=m, salary=100000.0, address=null)

  1. 爲Employee類添加serialVersionUID,序列化一個對象,修改serialVersionUID,反序列化該對象。

修改serialVersionUID:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
    private static final long serialVersionUID = 66666666666666L;//修改
    private String name;
    private Integer age;
    private char sex;
    private Double salary;
}

反序列化該對象:

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
Employee employee1 = (Employee) inputStream.readObject();
System.out.println(employee1);

程序拋出異常,異常信息以下:

Exception in thread "main" java.io.InvalidClassException: com.learn.java.extend.Employee; local class incompatible: stream classdesc serialVersionUID = 5815872246558374312, local class serialVersionUID = 66666666666666


  1. 爲Employee類添加serialVersionUID,序列化一個對象,爲其餘類添加相同的serialVersionUID,將對象反序列化爲其餘對象。

建立具備相同serialVersionUID的類:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Test implements Serializable {
    private static final long serialVersionUID = 5815872246558374312L;
    private String name;
    private Integer age;
    private char sex;
    private Double salary;
}

反序列化對象:

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
Test employee1 = (Test) inputStream.readObject();
System.out.println(employee1);

程序拋出異常,異常信息以下:

Exception in thread "main" java.lang.ClassCastException: com.learn.java.extend.Employee cannot be cast to com.learn.java.extend.Test


從以上3個測試的執行結果看,個人理解應該是對的。


更進一步

一旦類中添加了名爲serialVersionUID的靜態成員,那麼系列化系統就能夠讀入這個類的對象的不一樣版本。

「若是這個類只有方法產生了變化,那麼反序列化時不會有任何問題。可是,若是數據域發生了變化,那麼就可能會有問題。」 ——《Java核心技術》

事實上,上文部分代碼的執行結果已經放映了這一點,好比:先序列化一個Employee對象,而後在Employee類中添加address屬性,最後進行反序列化,獲得的對象信息爲:

Employee(name=xzy, age=22, sex=m, salary=100000.0, address=null)

在好比:先序列化一個Employee對象,而後修改Employee類的salary屬性,最後進行反序列化,獲得的對象信息爲:

Employee(name=xzy, age=22, sex=m, salary_=null)

舊版本的對象可能具備更多或更少的數據域,亦或者是數據域具備不一樣的類型。在這種狀況下,ObjectInputStream將盡力把舊版本對象轉換成類現有版本的對象。

ObjectInputStream會將這個類當前版本的數據域被序列化版本中數據域進行比較,固然,只會考慮非瞬時和非靜態的數據域。

若是,數據域名字匹配但類型不匹配:ObjectInputStream嘗試進行類型轉換。

若是,被序列版本具備現有版本所沒有的數據域:ObjectInputStream忽略這些額外的數據域。

若是,被序列版本缺乏現有版本所具備的數據域:ObjectInputStream將這些缺乏的數據域設置爲它們的默認值(若是是對象則是null,若是是數字則是0,若是是boolean則是false)

相關文章
相關標籤/搜索