序列化/反序列化,我忍你好久了

image

本文 Github開源項目:github.com/hansonwang9… 中已收錄,有詳細自學編程學習路線、面試題和麪經、編程資料及系列技術文章等,資源持續更新中...java


工具人

曾幾什麼時候,對於Java的序列化的認知一直停留在:「實現個Serializbale接口」不就行了的狀態,直到 ...git

因此此次抽時間再次從新捧起了塵封已久的《Java編程思想》,就像以前梳理《枚舉部分知識》同樣,把「序列化和反序列化」這塊的知識點又從新審視了一遍。程序員


序列化是幹啥用的?

序列化的本來意圖是但願對一個Java對象做一下「變換」,變成字節序列,這樣一來方便持久化存儲到磁盤,避免程序運行結束後對象就從內存裏消失,另外變換成字節序列也更便於網絡運輸和傳播,因此概念上很好理解:github

  • 序列化:把Java對象轉換爲字節序列。
  • 反序列化:把字節序列恢復爲原先的Java對象。

image

並且序列化機制從某種意義上來講也彌補了平臺化的一些差別,畢竟轉換後的字節流能夠在其餘平臺上進行反序列化來恢復對象。面試

事情就是那麼個事情,看起來很簡單,不事後面的東西還很多,請往下看。編程


對象如何序列化?

然而Java目前並無一個關鍵字能夠直接去定義一個所謂的「可持久化」對象。數組

對象的持久化和反持久化須要靠程序員在代碼裏手動顯式地進行序列化和反序列化還原的動做。網絡

舉個例子,假如咱們要對Student類對象序列化到一個名爲student.txt的文本文件中,而後再經過文本文件反序列化成Student類對象:ide

image

一、Student類定義函數

public class Student implements Serializable {

    private String name;
    private Integer age;
    private Integer score;
    
    @Override
    public String toString() {
        return "Student:" + '\n' +
        "name = " + this.name + '\n' +
        "age = " + this.age + '\n' +
        "score = " + this.score + '\n'
        ;
    }
    
    // ... 其餘省略 ...
}
複製代碼

二、序列化

public static void serialize( ) throws IOException {

    Student student = new Student();
    student.setName("CodeSheep");
    student.setAge( 18 );
    student.setScore( 1000 );

    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    objectOutputStream.close();
    
    System.out.println("序列化成功!已經生成student.txt文件");
    System.out.println("==============================================");
}
複製代碼

三、反序列化

public static void deserialize( ) throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();
    
    System.out.println("反序列化結果爲:");
    System.out.println( student );
}
複製代碼

四、運行結果

控制檯打印:

序列化成功!已經生成student.txt文件
==============================================
反序列化結果爲:
Student:
name = CodeSheep
age = 18
score = 1000
複製代碼

Serializable接口有何用?

上面在定義Student類時,實現了一個Serializable接口,然而當咱們點進Serializable接口內部查看,發現它居然是一個空接口,並無包含任何方法!

image

試想,若是上面在定義Student類時忘了加implements Serializable時會發生什麼呢?

實驗結果是:此時的程序運行會報錯,並拋出NotSerializableException異常:

image

咱們按照錯誤提示,由源碼一直跟到ObjectOutputStreamwriteObject0()方法底層一看,才恍然大悟:

image

若是一個對象既不是字符串數組枚舉,並且也沒有實現Serializable接口的話,在序列化時就會拋出NotSerializableException異常!

哦,我明白了!

原來Serializable接口也僅僅只是作一個標記用!!!

它告訴代碼只要是實現了Serializable接口的類都是能夠被序列化的!然而真正的序列化動做不須要靠它完成。


serialVersionUID號有何用?

相信你必定常常看到有些類中定義了以下代碼行,即定義了一個名爲serialVersionUID的字段:

private static final long serialVersionUID = -4392658638228508589L;
複製代碼

你知道這句聲明的含義嗎?爲何要搞一個名爲serialVersionUID的序列號?

繼續來作一個簡單實驗,還拿上面的Student類爲例,咱們並無人爲在裏面顯式地聲明一個serialVersionUID字段。

咱們首先仍是調用上面的serialize()方法,將一個Student對象序列化到本地磁盤上的student.txt文件:

public static void serialize() throws IOException {

    Student student = new Student();
    student.setName("CodeSheep");
    student.setAge( 18 );
    student.setScore( 100 );

    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    objectOutputStream.close();
}
複製代碼

接下來咱們在Student類裏面動點手腳,好比在裏面再增長一個名爲studentID的字段,表示學生學號:

image

這時候,咱們拿剛纔已經序列化到本地的student.txt文件,還用以下代碼進行反序列化,試圖還原出剛纔那個Student對象:

public static void deserialize( ) throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();
    
    System.out.println("反序列化結果爲:");
    System.out.println( student );
}
複製代碼

運行發現報錯了,而且拋出了InvalidClassException異常:

image

這地方提示的信息很是明確了:序列化先後的serialVersionUID號碼不兼容!

從這地方最起碼能夠得出兩個重要信息:

  • 一、serialVersionUID是序列化先後的惟一標識符
  • 二、默認若是沒有人爲顯式定義過serialVersionUID,那編譯器會爲它自動聲明一個!

第1個問題: serialVersionUID序列化ID,能夠當作是序列化和反序列化過程當中的「暗號」,在反序列化時,JVM會把字節流中的序列號ID和被序列化類中的序列號ID作比對,只有二者一致,才能從新反序列化,不然就會報異常來終止反序列化的過程。

第2個問題: 若是在定義一個可序列化的類時,沒有人爲顯式地給它定義一個serialVersionUID的話,則Java運行時環境會根據該類的各方面信息自動地爲它生成一個默認的serialVersionUID,一旦像上面同樣更改了類的結構或者信息,則類的serialVersionUID也會跟着變化!

因此,爲了serialVersionUID的肯定性,寫代碼時仍是建議,凡是implements Serializable的類,都最好人爲顯式地爲它聲明一個serialVersionUID明確值!

固然,若是不想手動賦值,你也能夠藉助IDE的自動添加功能,好比我使用的IntelliJ IDEA,按alt + enter就能夠爲類自動生成和添加serialVersionUID字段,十分方便:

image


兩種特殊狀況

  • 一、凡是被static修飾的字段是不會被序列化的
  • 二、凡是被transient修飾符修飾的字段也是不會被序列化的

對於第一點,由於序列化保存的是對象的狀態而非類的狀態,因此會忽略static靜態域也是理所應當的。

對於第二點,就須要瞭解一下transient修飾符的做用了。

若是在序列化某個類的對象時,就是不但願某個字段被序列化(好比這個字段存放的是隱私值,如:密碼等),那這時就能夠用transient修飾符來修飾該字段。

好比在以前定義的Student類中,加入一個密碼字段,可是不但願序列化到txt文本,則能夠:

image

這樣在序列化Student類對象時,password字段會設置爲默認值null,這一點能夠從反序列化所獲得的結果來看出:

image


序列化的受控和增強

約束性加持

從上面的過程能夠看出,序列化和反序列化的過程實際上是有漏洞的,由於從序列化到反序列化是有中間過程的,若是被別人拿到了中間字節流,而後加以僞造或者篡改,那反序列化出來的對象就會有必定風險了。

畢竟反序列化也至關於一種 「隱式的」對象構造 ,所以咱們但願在反序列化時,進行受控的對象反序列化動做。

那怎麼個受控法呢?

答案就是: 自行編寫readObject()函數,用於對象的反序列化構造,從而提供約束性。

既然自行編寫readObject()函數,那就能夠作不少可控的事情:好比各類判斷工做。

還以上面的Student類爲例,通常來講學生的成績應該在0 ~ 100之間,咱們爲了防止學生的考試成績在反序列化時被別人篡改爲一個奇葩值,咱們能夠自行編寫readObject()函數用於反序列化的控制:

private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {

    // 調用默認的反序列化函數
    objectInputStream.defaultReadObject();

    // 手工檢查反序列化後學生成績的有效性,若發現有問題,即終止操做!
    if( 0 > score || 100 < score ) {
        throw new IllegalArgumentException("學生分數只能在0到100之間!");
    }
}

複製代碼

好比我故意將學生的分數改成101,此時反序列化立馬終止而且報錯:

image

對於上面的代碼,有些小夥伴可能會好奇,爲何自定義的privatereadObject()方法能夠被自動調用,這就須要你跟一下底層源碼來一探究竟了,我幫你跟到了ObjectStreamClass類的最底層,看到這裏我相信你必定恍然大悟:

image

又是反射機制在起做用!是的,在Java裏,果真萬物皆可「反射」(滑稽),即便是類中定義的private私有方法,也能被摳出來執行了,簡直引發溫馨了。

單例模式加強

一個容易被忽略的問題是:可序列化的單例類有可能並不單例

舉個代碼小例子就清楚了。

好比這裏咱們先用java寫一個常見的「靜態內部類」方式的單例模式實現:

public class Singleton implements Serializable {

    private static final long serialVersionUID = -1576643344804979563L;

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }

    public static synchronized Singleton getSingleton() {
        return SingletonHolder.singleton;
    }
}
複製代碼

而後寫一個驗證主函數:

public class Test2 {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream(
                    new FileOutputStream( new File("singleton.txt") )
                );
        // 將單例對象先序列化到文本文件singleton.txt中
        objectOutputStream.writeObject( Singleton.getSingleton() );
        objectOutputStream.close();

        ObjectInputStream objectInputStream =
                new ObjectInputStream(
                    new FileInputStream( new File("singleton.txt") )
                );
        // 將文本文件singleton.txt中的對象反序列化爲singleton1
        Singleton singleton1 = (Singleton) objectInputStream.readObject();
        objectInputStream.close();

        Singleton singleton2 = Singleton.getSingleton();

        // 運行結果竟打印 false !
        System.out.println( singleton1 == singleton2 );
    }

}
複製代碼

運行後咱們發現:反序列化後的單例對象和原單例對象並不相等了,這無疑沒有達到咱們的目標。

解決辦法是:在單例類中手寫readResolve()函數,直接返回單例對象,來規避之:

private Object readResolve() {
    return SingletonHolder.singleton;
}
複製代碼

image

這樣一來,當反序列化從流中讀取對象時,readResolve()會被調用,用其中返回的對象替代反序列化新建的對象。


沒想到

本覺得這篇會很快寫完,結果又扯出了這麼多東西,不過這樣一梳理、一串聯,感受仍是清晰了很多。

就這樣吧,下篇見。

本文 Github開源項目:github.com/hansonwang9… 中已收錄,有詳細自學編程學習路線、面試題和麪經、編程資料及系列技術文章等,資源持續更新中...


慢一點,才能更快

相關文章
相關標籤/搜索