經過ArrayList底層源碼瞭解Java的序列化

序列化基礎知識

    關於序列化的用法及基礎知識,因爲不是本文重點,這裏再也不詳細介紹了,這裏只作簡單的介紹。java

  1.  在Java中,只要一個類實現了java.io.Serializable接口,那麼它就能夠被序列化。
  2.  經過ObjectOutputStreamObjectInputStream對對象進行序列化及反序列化。
  3.  虛擬機是否容許反序列化,不只取決於類路徑和功能代碼是否一致,一個很是重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID
  4. 序列化並不保存靜態變量。
  5.  要想將父類對象也序列化,就須要讓父類也實現Serializable 接口。
  6. transient 關鍵字的做用是控制變量的序列化,在變量聲明前加上該關鍵字,能夠阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。
  7. 服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,好比密碼字符串等,但願對該密碼字段在序列化時,進行加密,而客戶端若是擁有解密的密鑰,只有在客戶端進行反序列化時,才能夠對密碼進行讀取,這樣能夠必定程度保證序列化對象的數據安全。

ArrayList的序列化

    爲了深刻的介紹序列化,咱們這篇文章準備從Java源碼中的ArrayList類入手。看看Java自身是如何使用序列化的。在介紹ArrayList序列化以前,先來考慮一個問題:數組

    問:如何自定義的序列化和反序列化策略?安全

    帶着這個問題,咱們來看java.util.ArrayList的源碼服務器

    code1:dom

public class ArrayList<E> extends AbstractList<E>
       implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
   private static final long serialVersionUID = 8683452581122892189L;
   transient Object[] elementData; // non-private to simplify nested class access
   private int size;
}

        筆者省略了其餘成員變量,從上面的代碼中能夠知道ArrayList實現了java.io.Serializable接口,那麼咱們就能夠對它進行序列化及反序列化。由於負責保存元素的elementDatatransient的,因此咱們認爲這個成員變量的內容不會被序列化而保留下來。咱們寫一個Demo,驗證一下咱們的想法: 優化

  code2:加密

public static void main(String[] args) throws IOException, ClassNotFoundException {
       List<String> stringList = new ArrayList<String>();
       stringList.add("hello");
       stringList.add("world");
       stringList.add("hollis");
       stringList.add("chuang");
       System.out.println("init StringList" + stringList);
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("stringlist"));
       objectOutputStream.writeObject(stringList);

       IOUtils.close(objectOutputStream);
       File file = new File("stringlist");
       ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
       List<String> newStringList = (List<String>)objectInputStream.readObject();
       IOUtils.close(objectInputStream);
       if(file.exists()){
           file.delete();
       }
       System.out.println("new StringList" + newStringList);
   }
//init StringList[hello, world, hollis, chuang]
//new StringList[hello, world, hollis, chuang]

        瞭解ArrayList的人都知道,ArrayList底層是經過數組實現的。那麼數組elementData其實就是用來保存列表中的元素的。經過該屬性的聲明方式,咱們認爲,他應該是沒法經過序列化持久化下來的。spa

    問:爲何code 2的結果卻經過序列化和反序列化把List中的元素保留下來了呢?debug

writeObject和readObject方法

    在ArrayList中定義了來個方法: writeObjectreadObject。這裏先給出結論:code

        在序列化過程當中,若是被序列化的類中定義了writeObject readObject 方法,虛擬機會試圖調用對象類裏的 writeObjectreadObject 方法,進行用戶自定義的序列化和反序列化。若是沒有這樣的方法,則默認調用是 ObjectOutputStream defaultWriteObject 方法以及 ObjectInputStreamdefaultReadObject 方法。

        也就述說,用戶自定義的 writeObject readObject 方法能夠容許用戶控制序列化的過程,好比能夠在序列化的過程當中動態改變序列化的數值。咱們發現ArrayList中有這兩個方法的實現,那麼基本能夠肯定,elementData能被序列化持久下來,確定和這兩個方法有關,雖然他被聲明爲transitent,那麼咱們來看一下ArrayList類中這兩個方法的具體實現:

    code 3:

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
           ensureCapacityInternal(size);
           Object[] a = elementData;
           // Read in all elements in the proper order.
           for (int i=0; i<size; i++) {
               a[i] = s.readObject();
           }
       }
   }

    code4:

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();
       }
   }

     經過上面兩段代碼,咱們發現,raedObject方法和writeObjec方法中定義了關於elementData的序列化策略。如今,咱們能夠回答剛剛的問題了。

    問:爲何code 2的結果卻經過序列化和反序列化把List中的元素保留下來了呢?

    答:ArrayList中定義了raedObjectwriteObject方法,這兩個方法中定義了elementData的序列化及反序列化策略。

    那麼,問題又來了。

    問:爲何ArrayList要用這種方式來實現序列化呢?

    why transient

        ArrayList其實是動態數組,每次在放滿之後自動增加設定的長度值,若是數組自動增加長度設爲100,而實際只放了一個元素,那就會序列化99個null元素。爲了保證在序列化的時候不會將這麼多null同時進行序列化,ArrayList把元素數組設置爲transient

    why writeObject and readObject

        前面說過,爲了防止一個包含大量空對象的數組被序列化,爲了優化存儲,因此,ArrayList使用transient來聲明elementData。 可是,做爲一個集合,在序列化過程當中還必須保證其中的元素能夠被持久化下來,因此,經過重寫writeObject 和 readObject方法的方式把其中的元素保留下來。

writeObject方法把elementData數組中的元素遍歷的保存到輸出流(ObjectOutputStream)中。readObject方法從輸入流(ObjectInputStream)中讀出對象並保存賦值到elementData數組中。

至此,咱們先試着來回答剛剛提出的問題:

    問:爲何ArrayList要用這種方式來實現序列化呢?

    答:避免elementData數組中過多的無用的null被序列化。

    問:如何自定義的序列化和反序列化策略?

    答:能夠經過在被序列化的類中增長writeObjectreadObject方法。

    那麼問題又來了,雖然ArrayList中寫了writeObject readObject 方法,可是這兩個方法並無顯示的被調用啊。

    問:若是一個類中包含writeObject readObject 方法,那麼這兩個方法是怎麼被調用的呢?

ObjectOutputStream

    從code 4中,咱們能夠看出,對象的序列化過程經過ObjectOutputStreamObjectInputputStream來實現的,那麼帶着剛剛的問題,咱們來分析一下ArrayList中的writeObject readObject 方法究竟是如何被調用的呢?

    爲了節省篇幅,這裏給出ObjectOutputStreamwriteObject的調用棧:

    writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

    這裏看一下invokeWriteObject:

void invokeWriteObject(Object obj, ObjectOutputStream out)
       throws IOException, UnsupportedOperationException
   {
       if (writeObjectMethod != null) {
           try {
               writeObjectMethod.invoke(obj, new Object[]{ out });
           } catch (InvocationTargetException ex) {
               Throwable th = ex.getTargetException();
               if (th instanceof IOException) {
                   throw (IOException) th;
               } else {
                   throwMiscException(th);
               }
           } catch (IllegalAccessException ex) {
               // should not occur, as access checks have been suppressed
               throw new InternalError(ex);
           }
       } else {
           throw new UnsupportedOperationException();
       }
   }

        其中writeObjectMethod.invoke();是關鍵,經過反射的方式調用writeObjectMethod方法。官方是這麼解釋這個writeObjectMethod的:

        class-defined writeObject method, or null if none 

        在咱們的例子中,這個方法就是咱們在ArrayList中定義的writeObject方法。經過反射的方式被調用了。

至此,咱們先試着來回答剛剛提出的問題:

    問:若是一個類中包含writeObject readObject 方法,那麼這兩個方法是怎麼被調用的?

    答:在使用ObjectOutputStreamwriteObject方法和ObjectInputStreamreadObject方法時,會經過反射的方式調用。

    至此,咱們已經介紹完了ArrayList的序列化方式。那麼,不知道有沒有人提出這樣的疑問:

    問:Serializable明明就是一個空的接口,它是怎麼保證只有實現了該接口的方法才能進行序列化與反序列化的呢?

    Serializable接口的定義:

public interface Serializable {
}

        若是嘗試對一個未實現Serializable接口的類進行序列化,會拋出java.io.NotSerializableException。這是爲何呢?Serializable只是一個空接口,如何實現的呢?

        其實這個問題也很好回答,咱們再回到剛剛ObjectOutputStreamwriteObject的調用棧:

        writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

        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() + "\n" + debugInfoStack.toString());
       } else {
           throw new NotSerializableException(cl.getName());
       }
   }

     在進行序列化操做時,會判斷要被序列化的類是不是Enum、ArraySerializable類型,若是不是則直接拋出NotSerializableException

    問:Serializable明明就是一個空的接口,它是怎麼保證只有實現了該接口的方法才能進行序列化與反序列化的呢?

    答:在類的序列化過程當中,會使用instanceof關鍵字判斷一個類是否繼承了Serializable類,若是沒有,則直接拋出NotSerializableException異常。

總結 

    一、若是一個類想被序列化,須要實現Serializable接口。不然將拋出NotSerializableException異常,這是由於,在序列化操做過程當中會對類型進行檢查,要求被序列化的類必須屬於Enum、ArraySerializable類型其中的任何一種。

    二、在變量聲明前加上該關鍵字,能夠阻止該變量被序列化到文件中。

    三、在類中增長writeObject readObject 方法能夠實現自定義序列化策略。

相關文章
相關標籤/搜索