1. 程式人生 > 實用技巧 >serialVersionUID序列化版本號與ObjectOutputStream物件輸入輸出流

serialVersionUID序列化版本號與ObjectOutputStream物件輸入輸出流

  1. 我們觀察ObjectOutputStream就可以發現該類沒有無參構造,只有有參構造,所以是一個包裝流
  2. 具體使用:
    public static void main(String[] args) throws IOException {
        Student stu = new Student(13,"xj",15,"男");

        String path = "src/main/java/com/xj/dayio/demo2.txt";
        FileOutputStream fos = new FileOutputStream(path);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(stu);
    }
  1. 我們在使用ObjectOutputStream的時候,需要為被序列化的物件實現Serializable介面(implements Serializable),意思是此類是“可序列化的”。

否則會丟擲異常:Exception in thread "main" java.io.NotSerializableException: com.xj.dayio.Student

    public class Student implements Serializable {

    private int id;
    private String name;
    private int age;
    private String sex;

    構造器。。。

    get,set方法

    toString方法
}

此時呼叫ObjectOutputStream物件輸出流就可以成功將物件寫出到檔案

�� sr com.xj.dayio.Student�(U�\jb� I ageI idL namet Ljava/lang/String;L sexq ~ xp
t
xjt 男

  1. 我們在讀取的時候也可以使用ObjectInputStream物件輸入流來進行讀取
    public static void main(String[] args) throws IOException {

        String path = "src/main/java/com/xj/dayio/demo2.txt";
        FileInputStream fis = new FileInputStream(path);
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object object = ois.readObject();
        System.out.println((Student) object);
    }

輸出:

Student{id=13, name='xj', age=15, sex='男'}

  1. 那麼我們的序列化介面是主要做什麼呢?為什麼必須要實現接口才能使用物件輸出流

    • 序列化指的是把一個物件中所包含的資料按照一定的順序轉化成二進位制資料。
    • 但是,我們自己定義的型別,JVM沒有權力直接對其物件進行序列化。因為一個型別一旦允許被序列化,則意味著該型別物件中儲存的資料存在洩露的風險。
    • 程式設計師雖然不需要關心物件的具體序列化過程,但是必須要向JVM授權,告訴它哪個型別是允許被序列化的。
    • 向JVM授意的方式:讓需要被傳輸的型別實現一個介面Serializable,該介面代表“可序列化的”。
    • Serializable是一個空介面,什麼內容都沒有。它只是為了起到一個“標識”作用。

注意:需要被IO流傳輸的物件中所有包含的型別都需要是“可序列化”的。所有的引用型別屬性所屬型別都必須實現Serializable。(那為什麼String物件我們可以直接序列化呢?我們檢視String的原始碼就可以發現String類底層已經實現過序列化介面)

  1. 在反序列化的時候,需要提供對應的.class檔案。

    • 例如本案例中,寫入到檔案中的是一個com.xj.dayio.Student類物件
    • 該物件所有包含的資訊都會被寫入到檔案中,也包含它的型別資訊。
    • 在反序列化時,JVM會嘗試載入這個型別。
    • 如果對應的.class不存在,則類載入失敗,反序列化對應也會失敗。
    • 因為記憶體中想要出現一個物件,它所屬的型別不可能不存在。
  2. 需要提供的.class位元組碼檔案未必一定是當時儲存時!

我們可以將Student物件輸出到檔案中後,然後刪除掉Student類,然後重寫一個Student類,但是這個類必須要與原Student類一樣

包括包名、類名、實現的介面、繼承的父類、屬性、屬性的修飾符、屬性的名稱、屬性的型別、屬性的預設值、方法、方法的修飾符、方法的返回值、方法的引數、方法的丟擲異常……

  1. 實際上,我們看到的只是一個表象。
    決定是否允許進行反序列化,實際上並不是單純地、直接地根據類中包含的資訊進行判斷。

    實際上,是由一個叫做serialVersionUID(序列化版本號)欄位控制反序列化是否允許執行。

向檔案中寫入一個物件的時候,也包含一個UID。將來進行反序列化的時候,也會將這個UID讀取出來,並且和當前嘗試載入的類的該UID欄位值進行比較。

  • 如果這兩個UID是相同的,則允許執行反序列化,生成物件成功。
  • 如果這兩個UID是不相同的,則不允許執行反序列化,生成物件失敗。會丟擲異常:java.io.InvalidClassException - 不合法的型別異常
  1. 那麼serialVersionUID來自哪裡呢,我們在程式中並沒有寫呀。
  • 如果我們在類中沒有定義該欄位,那麼JVM會自動計算出一個值來充當該類的該欄位值
  • 該欄位值的計算過程類似雜湊過程。意味著只要參與運算的數值相同,那麼結果也一定相同。
  • 也就是說,只要兩個類保證一模一樣,計算出來的serialVersionUID肯定是相同的。
  1. 但是同樣我們也可以自己定義serialVersionUID
    只需要在程式碼中加上這一句即可
    public class Student implements Serializable {

    private static final long serialVersionUID = -4167987148983999791L;

    private int id;
    private String name;
    private int age;
    private String sex;

我們還可以有idea直接生成,但是idea預設是不會提醒的
我們需要修改設定,將其後面的對勾勾上

這時如果我們沒有寫serialVersionUID,ide就會爆出警告

根據提示ide就會自動為我們生成預設的serialVersionUID

  1. 我們剛才說了,反序列化是主要根據serialVersionUID 來的,與屬性,方法等無關,只是預設serialVersionUID 是由屬性,方法生成,但是我們在手動定義serialVersionUID 後,JVM就不會使用預設的,而是使用我們自己的。我們可以定義一個serialVersionUID 為1,那麼此時我無論是刪除屬性,修改方法等,都不會影響反序列化,只是我們如果屬性對應不上就無法賦值
    例如:若我將name重改為name1
    輸出:此時name屬性就沒有被賦值,這也是為什麼序列化後資料會有洩露的風險

Student{id=13, name='null', age=15, sex='男'}

比如儲存使用者名稱和密碼的檔案被洩露,此時我只需要寫一個類,實現Serializable 後,裡面定義很多個變數,如password,passWord,pwd,PassWord等等,此時使用我自己定義的類去使用物件流去讀取儲存密碼的檔案時,就有可能將密碼提取出來,所以我們應該儘可能的去讓序列化版本號serialVersionUID去隨機,這樣子,序列化版本號不正確,檔案也無法被反序列化。