淺談Java序列化

scu醬油仔發表於2019-01-19

Java序列化

什麼是序列化?

序列化是將一個物件的狀態,各屬性的值序列化儲存起來,然後在合適的時候通過反序列化獲得。

Java的序列化是將一個物件表示成位元組序列,該位元組序列包括了物件的資料,有關物件的型別資訊和儲存在物件中的資料型別。

說白了,就是將物件儲存起來,就跟儲存字串資料一樣,用到的時候再取出來。任何實現了Serializable介面的類都可以被序列化。

實現Serializable介面進行序列化

package com.wangjun.othersOfJava;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SerializeDemo {

    public static void main(String[] args) {
        Employee em = new Employee();
        em.name = "wangjun";
        em.age = 24;
        em.ssh = 123456;
        // 將物件序列化後儲存到檔案
        try (
                FileOutputStream fo = new FileOutputStream("tem.ser");
                ObjectOutputStream oo = new ObjectOutputStream(fo))
        {
            oo.writeObject(em);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 反序列化取出物件
        try(
                FileInputStream fi = new FileInputStream("tem.ser");
                ObjectInputStream oi = new ObjectInputStream(fi)) 
        {
            Employee e2 = (Employee) oi.readObject();
            System.out.println(e2.name);
            System.out.println(e2.age);
            System.out.println(e2.ssh);
            System.out.println(Employee.local);
            e2.test();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class Employee implements Serializable {
        String name;
        int age;
        static String local = "earth";
        transient int ssh;

        public void test() {
            System.out.println("this is test method!");
        }
    }

}

程式的執行結果:

wangjun
24
0
earth
this is test method!

如果有一些欄位不想被序列化怎麼辦呢?這時候就可以用transient關鍵字修飾,就像上面程式碼的ssh欄位,關於transient關鍵字有以下幾個特點:

  • 一旦被transient關鍵字修飾,那變數將不再是物件持久化的一部分,該變數內容在序列化後無法獲得訪問;
  • transient只能修飾變數,不能修飾方法和類,本地變數(區域性變數)也不能被transient修飾;
  • 一個靜態變數不管是否被transient修飾,都不能被序列化。

從上面的例子看到好像與第三條不符,其實反序列化取出的local是JVM裡面的值,而不是反序列化出來的。可以加一行程式碼驗證一下,在反序列化之前更改一下local的值:

// 反序列化取出物件
Employee.local = "earth2";
try(
  ...

看一下列印結果

wangjun
24
0
earth2
this is test method!

這說明列印出來的是JVM中對應的local的值earth2,而不是序列化的時候的值earth。

實現Externalizable介面進行序列化

transient只有對實現了Serializable介面方式的序列化有效,還有一種序列化的方式是實現Externalizable介面,這種實現方式不像實現Serializable介面一樣可以幫你自動序列化,它需要在writeExternal方法中手動指定需要序列化的變數並且在readExternal手動取出來,這與是否被transient修飾無關,下面更改一下上面的例子,將Employee類改成:

static class Employee implements Externalizable {
        String name;
        int age;
        static String local = "earth";
        transient int ssh;
        
        //實現Externalizable介面進行序列化必須顯式宣告無參構造器
        public Employee() {
        }

        public void test() {
            System.out.println("this is test method!");
        }

        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeObject(name);
            //out.writeObject(age);
            out.writeObject(ssh);
            out.writeObject(local);
        }

        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            name = (String) in.readObject();
            //age = (int) in.readObject();
            ssh = (int) in.readObject();
            local = (String) in.readObject();
        }
    }

重新執行,結果(注意:上述主函式中還存在對local重新賦值的程式碼Employee.local = "earth2";):

wangjun
0
123456
earth
this is test method!

可以看到能否被序列化跟transient和static修飾都沒有關係,只跟writeExternal和readExternal有關係。

Serializable和Externalizable的區別

  • 對Serializable物件反序列化時,由於Serializable物件完全以它儲存的二進位制位為基礎來構造,因此並不會呼叫任何建構函式,因此Serializable類無需預設建構函式,但是當Serializable類的父類沒有實現Serializable介面時,反序列化過程會呼叫父類的預設建構函式,因此該父類必需有預設建構函式,否則會拋異常。
  • 對Externalizable物件反序列化時,會先呼叫類的不帶引數的構造方法,這是有別於預設反序列方式的。如果把類的不帶引數的構造方法刪除,或者把該構造方法的訪問許可權設定為private、預設或protected級別,會丟擲java.io.InvalidException: no valid constructor異常,因此Externalizable物件必須有預設建構函式,而且必需是public的。
  • 如果不是特別堅持實現Externalizable介面,那麼還有另一種方法。我們可以實現Serializable介面,並新增writeObject()readObject()的方法。一旦物件被序列化或者重新裝配,就會分別呼叫那兩個方法。也就是說,只要提供了這兩個方法,就會優先使用它們,而不考慮預設的序列化機制。

SerialVersionUID的作用

上述實現Serializable介面的Employee類中,會有一個警告:

The serializable class Employee does not declare a static final serialVersionUID field of type long

意思是Employee沒有宣告一個靜態final的常量serialVersionUID,那這個serialVersionUID的作用是什麼呢?

serialVersionUID是對類進行版本控制的,Java的序列化機制是通過判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的位元組流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常,即是InvalidCastException。

serialVersionUID有兩種生成方式:

  • 一是預設的1L,比如:private static final long serialVersionUID = 1L;
  • 二是根據類名、介面名、成員方法及屬性等來生成一個64位的雜湊欄位。

如果程式沒有顯式的宣告serialVersionUID,那麼程式將用第二種實現。我們可以做一個實現,還是用上述實現Serializable介面的例子。

我們先執行一下程式,生成序列化檔案tem.ser,在把“將物件序列化後儲存到檔案”這一段邏輯註釋掉,對Employee類增加一個test欄位:

static class Employee implements Serializable {
  String name;
  int age;
  static String local = "earth";
  transient int ssh;
  String test;

  public void test() {
    System.out.println("this is test method!");
  }
}

這時候執行的時候會報錯:

java.io.InvalidClassException: com.wangjun.othersOfJava.SerializeDemo$Employee; local class incompatible: stream classdesc serialVersionUID = 4506166831890198488, local class serialVersionUID = 785960679919880606
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2000)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
    at com.wangjun.othersOfJava.SerializeDemo.main(SerializeDemo.java:32)

因為程式發現取到的序列化檔案的serialVersionUID和當前的serialVersionUID不一樣。這個serialVersionUID是根據類名、介面名、成員方法及屬性等來生成一個64位的雜湊欄位,因為增加了test欄位,因此生成的serialVersionUID不一樣了。

接著,我們顯式的宣告serialVersionUID

static class Employee implements Serializable {
  private static final long serialVersionUID = 1L;
  String name;
  int age;
  static String local = "earth";
  transient int ssh;

  public void test() {
    System.out.println("this is test method!");
  }
}

將剛才註釋的程式碼取消註釋,執行一遍再註釋掉,並且新增欄位test:

static class Employee implements Serializable {
  private static final long serialVersionUID = 1L;
  String name;
  int age;
  static String local = "earth";
  transient int ssh;
  String test;

  public void test() {
    System.out.println("this is test method!");
  }
}

再次執行發現沒有報錯,執行OK。這是因為你顯式宣告瞭serialVersionUID,序列化的serialVersionUID和目前的serialVersionUID一樣,因此會認為是同一個版本的類。

你也可以將serialVersionUID改成2L,這個時候又會報錯了。

參考:

https://www.cnblogs.com/duanx…

https://blog.csdn.net/fjndwy/…

https://blog.csdn.net/bigtree…

相關文章