秒懂Java序列化與反序列化

版權申明】非商業目的可自由轉載
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/80629348
出自:shusheng007

概述

什麼是序列化?什麼是反序列化?爲什麼需要序列化?如何序列化?應該注意什麼?本文將從這幾方面來論述。

定義

什麼是序列化?什麼是反序列化?

序列化: 把Java對象轉換爲字節序列的過程。
反序列化:把字節序列恢復爲Java對象的過程。

作用

爲什麼需要序列化?

在當今的網絡社會,我們需要在網絡上傳輸各種類型的數據,包括文本、圖片、音頻、視頻等, 而這些數據都是以二進制序列的形式在網絡上傳送的,那麼發送方就需要將這些數據序列化爲字節流後傳輸,而接收方接到字節流後需要反序列化爲相應的數據類型。當然接收方也可以將接收到的字節流存儲到磁盤中,等到以後想恢復的時候再恢復。

綜上,可以得出對象的序列化和反序列化主要有兩種用途:

  • 把對象的字節序列永久地保存到磁盤上。(持久化對象)
  • 可以將Java對象以字節序列的方式在網絡中傳輸。(網絡傳輸對象)

如何實現

如何序列化和反序列化?

如果要讓某個對象支持序列化機制,則其類必須實現下面這兩個接口中任一個。

  • Serializable

    public interface Serializable {
    }
  • Externalizable

    public interface Externalizable extends java.io.Serializable {
       void writeExternal(ObjectOutput out) throws IOException;
       void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
     }

實現Serializable接口

簡單實現

如果是對序列化的需求非常簡單,沒有對序列化過程控制的需求,可以簡單實現Serializable接口即可。
Serializable的源碼可知,其是一個標記接口,無需實現任何方法。例如我們有如下的Student

    public class Student implements Serializable {  
        private String name;
        private int age;

        public Student(String name,int age)
        {
            System.out.println("有參數構造器執行");
            this.name=name;
            this.age=age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }

序列化: 那麼我們如何將此類的對象序列化後保存到磁盤上呢?

  1. 創建一個 ObjectOutputStream 輸出流oos
  2. 調用此輸出流ooswriteObject()方法
private static void serializ()
{
    try (ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.txt"));)
    {
       Student s=new Student("ben",18);
       oos.writeObject(s);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

上面代碼將Sutent 的一個實例對象序列化到了一個文本文件中。

反序列化:我們如從文本文件中將此對象的字節序列恢復成Student對象呢?

  1. 創建一個ObjectInputStream 輸入流ois
  2. 調用此輸入流oisreadObject()方法。

     private static void deSerializ()
     {
         try(ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.txt"));)
         {
             Student s= (Student) ois.readObject();
             System.out.println(s.toString());
         }catch (Exception e)
         {
             e.printStackTrace();
         }
     }

    Node: 當反序列化的時候並沒有調用Student的構造函數,說明反序列化機制無需通過構造器來構建Java對象,這就給實現了序列化機制的單例模式造成了麻煩。

版本 serialVersionUID

由於反序列化Java對象的時候,必須提供該對象的class文件,但是隨着項目的升級class文件文件也會升級,Java如何保證兼容性呢?答案就是 serialVersionUID。每個可以序列化的類裏面都會存在一個serialVersionUID,只要這個值前後一致,即使類升級了,系統仍然會認爲他們是同一版本。如果我們不顯式指定一個,系統就會使用默認值。

```
public class Student implements Serializable {
    private static final long serialVersionUID=1L;
    ...
}
```

我們應該總是顯式指定一個版本號,這樣做的話我們不僅可以增強對序列化版本的控制,而且也提高了代碼的可移植性。因爲不同的JVM有可能使用不同的策略來計算這個版本號,那樣的話同一個類在不同的JVM下也會認爲是不同的版本。

那麼我們如何維護這個版本號呢?

  • 只修改了類的方法,無需改變serialVersionUID
  • 只修改了類的static變量和使用transient 修飾的實例變量,無需改變serialVersionUID
  • 如果修改了實例變量的類型,例如一個變量原來是int改成了String,則反序列化會失敗,需要修改serialVersionUID;如果刪除了類的一些實例變量,可以兼容無需修改;如果給類增加了一些實例變量,可以兼容無需修改,只是反序列化後這些多出來的變量的值都是默認值。

繼承及引用對象序列化

當要序列化的類存在父類的時候,直接或者間接福來,其父類也必須可以序列化。

當要序列化的類中引用了其他類的對象,那麼這些對象的類也必須是可序列化的,如下面代碼中的Teacher 類也必須是可以序列化的

public class Student implements Serializable {    
    private Teacher teacher;
    ...
}

Java序列化算法

Java序列化遵循以下算法:

  • 所有序列化過的,包括磁盤中的的實例對象都有一個序列化編號
  • 當試圖序列化一個對象時,程序會先檢查該對象是否已經被序列化過,當對象在本次虛擬機中從未被序列化過,則系統將其序列化爲字節序列並輸出
  • 如果某個對象在本次虛擬機中已經序列化過,則直接輸出這個序列化編號

鑑於以上的算法可能會造成一個潛在的問題:當序列化一個可變對象時,只有第一次使用writeObject()方法輸出時纔會輸出字節序列,而第二次調用時僅僅輸出一個序列化編號,即使我們改變了這個對象的一些屬性,這些改變後的屬性也不會序列化到磁盤上,這點在開發中需要非常注意。下面我們看一下代碼:

private static void reSerialize()
{
    try(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("student.txt"));
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("student.txt"));)
    {
        Student s=new Student("ben",18);
        oos.writeObject(s);
        Student rs1= (Student) ois.readObject();

        s.setAge(32);
        oos.writeObject(s);
        Student rs2= (Student) ois.readObject();

        System.out.println("兩個對象是否相等:"+ (rs1==rs2));
        System.out.println("希望年齡變爲32:"+rs2.getAge());
    }catch (Exception e)
    {
        e.printStackTrace();
    }
}

輸出結果:

兩個對象是否相等:true
希望年齡變爲3218

從輸出結果可以看出,修改前後反序列化出來的兩個對象時絕對相等的,輸出的其實是第一個對象,而且我們隊年齡做的修改也沒有生效。

自定義序列化

  • 通過tansient阻止實例變量的序列化。

    Java默認會序列化所有的實例變量,如果我們不想序列化某一個實例變量,就可以使用tansient這個關鍵字修飾。

    private transient String name;
  • 通過writeObject()readObject()方法控制序列化過程
    只需要爲實現了Serializable接口的類提供兩個如下簽名的方法,就可完全控制序列化和發序列化過程。

       private void writeObject(ObjectOutputStream out) throws IOException
       private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException

    例如我們給前面介紹的Student類添加兩個如下方法。

       private void writeObject(ObjectOutputStream out) throws IOException
       {
           out.writeObject("hello "+name);
           out.writeInt(age);
       }
    
       private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException
       {
           name= (String) in.readObject();
           age=in.readInt();
       }

    那麼反序列化後name 屬性的值就會加上hello 前綴。

  • 通過writeReplace()方法控制序列化過程

    爲實現了Serializable接口的類提供 如下簽名的方法

    Any-Access-Modifier Object writeReplace() throws ObjectStreamException

    該方法在開始序列化writeObject()之前執行,所以可以在序列化對象之前對要序列化的對象做一些處理,甚至完全替換掉原來的對象。 例如下面的代碼無論被序列化的對象是什麼,反序列化出來的對象總是一個字符串“總有刁民想害朕”。

      private Object writeReplace() throws ObjectStreamException{
          return "總有刁民想害朕";
      }
  • 通過readResolve()方法控制反序列化過程

    爲實現了Serializable接口的類提供 如下簽名的方法

    Any-Access-Modifier Object readResolve() throws ObjectStreamException

    該方法在反序列化readObject()後執行,所以可以在反序列化後對獲得的對象做一些處理,甚至完全替換爲其他對象。例如下面代碼無論反序列化後得到的對象是什麼,都會被替換成一個字符串”昏君人人得而誅之”。

       private Object readResolve() throws ObjectStreamException{
            return "昏君人人得而誅之";
       }

    這個函數在單例類實現序列化時特別有用,通過前面的介紹 我們知道,通過序列化可以不使用構造函數而獲取一個類的實例,這樣的話一個單例類就會存在兩個實例了,就失去效用了。那麼如何解決這個問題呢?

    1、最好是使用枚舉enum來構建一個單例,這是最好的方法,解決了序列化以及反射生成實例的問題。

    public enum Singleton {
        INSTANCE;
    }

    2、如果只是解決由於序列化導致的單例破壞問題,可以使用readResolve()方法解決,如下代碼所示:

    public class Singleton implements Serializable{
        public static final Singleton INSTANCE = new Singleton();
        private Singleton() {
        }
        protected Object readResolve() {
            return INSTANCE;
        }
        ...
    }

實現Externalizable接口

如果採用這種方式的話,序列化過程必須完全由程序員自己完成,看如下代碼:

public class Teacher implements Externalizable{
    private String name;
    private Integer age;
    public Teacher(String name,Integer age){
        System.out.println("有參構造");
        this.name = name;
        this.age = age;
    }
    //setter、getter方法省略

    //編寫自己的序列化邏輯
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject("hello:"+name); //將name加上前綴
        out.writeInt(age);  //注掉這句後,age屬性將不能被序化
    }

    //編寫自己的反序列化邏輯
    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        name = ((StringBuffer) in.readObject()).reverse().toString();
        age = in.readInt();  
    }

    @Override 
    public String toString() {  
        return "[" + name + ", " + age+ "]";  
    } 
}

可見Externalizable將序列化和反序列化的工作完全交給了程序員,那樣的好處就是自由度變大,如果碰上牛逼程序員,效率也會提升,碰上傻逼程序員就真的傻逼了。鑑於多年編程經驗,一般情況下還是使用Serializable較爲穩妥,和開發效率比起來,性能就是個屁,不然Java之類的語言也不會打敗C++。

結語

每次寫完一個主題的總結文章就感覺相關分知識其實不難也不多,爲什麼我以前感覺那麼難那麼多呢?只能說明自己知道的還是不夠多,需要繼續努力。等以後對這部分知識有了新的認識再來更新。

參考文章: https://blog.csdn.net/zcl_love_wx/article/details/52126876

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章