1. 程式人生 > >秒懂Java序列化與反序列化

秒懂Java序列化與反序列化

概述

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

定義

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

序列化: 把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++。

結語

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