Java的序列化與反序列化

呼延十發表於2019-01-29

前言

Java的序列化與反序列化是Java中比較重要的一個知識,本文將總結一下,怎麼使用序列化功能以及經常遇到的一些問題的解答.

什麼是Java的序列化

JDK提供給我們的,可以將某一個物件轉化為二進位制位元組流儲存,並從位元組流恢復物件的一種技術.

我們可以再網路傳輸物件,或者持久化物件時使用這項技術.

怎麼進行序列化與反序列化

Java中通過繼承Serializable介面來獲得序列化與反序列化的能力,使用ObjectInputStream和ObjectOutputStream來進行具體的物件序列化讀寫.

示例如下:

package daily;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;

/**
 * created by huyanshi on 2019/1/29
 */
public class SerializTest implements Serializable {

  private static int staticValue = 10;

  private int value;

  SerializTest(int value) {
    this.value = value;
  }

  public static void main(String[] args) {
    try {
      //初始化
      SerializTest test = new SerializTest(100);
      //序列化
      ObjectOutputStream oos = new ObjectOutputStream(
          new FileOutputStream("/Users/pfliu/Desktop/serialized.ser"));
      System.out.println(test.value);
      System.out.println(SerializTest.staticValue);
      oos.writeObject(test);

      SerializTest.staticValue = 250;

      //反序列話
      ObjectInputStream ois = new ObjectInputStream(
          new FileInputStream("/Users/pfliu/Desktop/serialized.ser"));
      SerializTest test1 = (SerializTest) ois.readObject();
      System.out.println(test1.value);
      System.out.println(SerializTest.staticValue);


    } catch (Exception e) {
      System.out.println("error");
    }
  }
}

在上面的程式碼中,我們new了一個物件,並將其進行了序列化與反序列化,並在序列化之前和反序列化之後列印了物件的值,結果為值相同.同時,在桌面上生成了Serialized.set檔案.

為什麼必須要實現Serializable介面?

點開該介面的原始碼,我們可以發現,這是一個空的介面,即沒有任何的定義,那麼它是怎麼使用的呢?

在序列化的過程中,我們會呼叫ObjectOutputStreamwriteObject方法,該方法,該方法呼叫writeObject0方法,該方法中有如下程式碼:

if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
} else {
    if (extendedDebugInfo) {
        throw new NotSerializableException(
            cl.getName() + "
" + debugInfoStack.toString());
    } else {
        throw new NotSerializableException(cl.getName());
    }
}

可以看到,對傳入的物件進行了幾次判斷,分別判斷傳入物件是否為:String,Array,Enum,Serializable.

什麼意思呢?就是JDK規定了,只有字串,陣列,列舉,Serializable四種物件才允許序列化,其他的都會丟擲NotSerializableException異常.

而這四種中,前面三種都是內定的,只有最後一種是留給程式設計師的序列化通道,因此我們想要序列化某一個類,必須實現Serializable介面.

序列化ID是幹什麼用的?

在看一些開源框架的程式碼時,發現他們的類都會有private static final long serialVersionUID = 8683452581122892189L;這個屬性,這是用來幹什麼的呢?

序列化和反序列化的匹配是怎麼匹配的?總不能隨便來的吧,A類序列化後的二進位制檔案,B類能從哪裡讀出一個物件來嘛?

不能,類的路徑以及功能程式碼必須完全相同,而序列化ID也是用來補充這一判斷的.

試想一下,你在服務裡new了一個物件,並將其序列化使用網路傳輸,那麼收到這個二進位制流的人都能序列化嗎?不是的,他必須在自己的服務中有同樣的類路徑,同樣的類定義,同時,他的類中定義的序列化ID必須與你的一致才可以.算是一定程度上的安全性保證吧.

當然,日常開發中我們使用預設生成的1L即可.

靜態變數的序列化

我在上面的程式碼中,定義了一個靜態變數,他也能被序列化嗎?

在序列化之後,對靜態變數重新賦值,那麼兩次列印的值相等嗎?

列印結果是:

10
250

為什麼呢?這個問題其實比較簡單,靜態變數是屬於類的,而我們是序列化了物件,因此不包含類的靜態變數是正常的.

transient 關鍵字

transient 關鍵字用於在序列化時,忽略某一個欄位,在反序列化後該欄位為初始值,比如int=0,物件引用為null.

ArrayList 的序列化

看了這麼多理論知識,我們來看一下常用類ArrayList是怎麼序列化的吧.

ArrayList實現了Serializable自然不必多說,其中用來儲存資料的屬性定義為:

/**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */
transient Object[] elementData; // non-private to simplify nested class access

為什麼會定義為transient呢?我序列化一個ArrayList,你不給我儲存內部的值?我要你個空殼子幹啥!我摔!

穩住,我們可以實際測試一下,會發現在序列化及反序列化的過程中,是保留了list中的值的.

為什麼要定義為transient呢?怎麼做到仍然保留資料的呢?

第一個問題

ArrayList內部是使用陣列實現的,雖然他是動態陣列,但是也是陣列.

也就是說,當你定義了長度為100的Arraylist,只放入了一個物件,剩下的99個就為空了.

序列化的時候有必要將這99個空也記錄下來嗎?沒有.因此定義為了transient.

第二個問題

在序列化的過程中,虛擬機器會試圖呼叫被序列化類writeObject和readObject方法,呼叫不到才會去執行預設的這兩個方法,也就是對應的輸入輸出流中的方法.

在ArrayList中有如下程式碼:

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

/**
 * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
 * deserialize it).
 */
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

可以在程式碼中看到,ArrayList自定義的序列化方法,沒有序列化99個空值,只序列化了有意義的值.

總結

1.java的序列化需要實現Serializable介面,之後使用ObjectOutputStreamObjectInputStream進行讀寫.
2.必須實現Serializable是因為JDK中進行了檢查,不屬於那四個類就會拋異常且不允許序列化.
3.序列化ID可以起到驗證是不是同一個類的作用,當然是在兩個類的程式碼完全一樣的基礎上.
4.transient關鍵字可以忽略一些欄位,使其不參與序列化.
5.靜態變數是不會序列化的,因為序列化的是物件,而靜態變數屬於.
6.可以參考ArrayList的實現方法實現自己的自定義序列化,在這個自定義的過程中,可以做許多事情,比如對某些欄位加密(常用語密碼欄位).

參考連結

https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html
https://www.hollischuang.com/archives/1140

完.

ChangeLog

2019-01-28 完成

以上皆為個人所思所得,如有錯誤歡迎評論區指正。

歡迎轉載,煩請署名並保留原文連結。

聯絡郵箱:huyanshi2580@gmail.com

更多學習筆記見個人部落格——>呼延十


相關文章