1. 程式人生 > >你真的以為你瞭解Java的序列化了嗎?

你真的以為你瞭解Java的序列化了嗎?

                                你真的以為你瞭解Java的序列化了嗎?

 

轉載:https://mp.weixin.qq.com/s/ABtxdNpr4bLpXtFiOK47hA

怎麼實現Java的序列化?

為什麼實現了java.io.Serializable接口才能被序列化?

transient的作用是什麼?

如果在序列化中破壞transitent的限制?

怎麼自定義序列化策略?

自定義的序列化策略是如何被呼叫的?

ArrayList對序列化的實現有哪些好處?

序列化基礎知識

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的原始碼

code 1

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介面,那麼我們就可以對它進行序列化及反序列化。因為負責儲存元素的elementData是transient的,所以我們認為這個成員變數的內容不會被序列化而保留下來。我們寫一個Demo,驗證一下我們的想法:

code 2

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其實就是用來儲存列表中的元素的。通過該屬性的宣告方式,我們認為,他應該是無法通過序列化持久化下來的。
 


問:為什麼code 2的結果卻通過序列化和反序列化把List中的元素保留下來了呢?

writeObject和readObject方法

在ArrayList中定義了來個方法: writeObjectreadObject

這裡先給出結論:

在序列化過程中,如果被序列化的類中定義了writeObject 和 readObject 方法,虛擬機器會試圖呼叫物件類裡的 writeObject 和 readObject 方法,進行使用者自定義的序列化和反序列化。

如果沒有這樣的方法,則預設呼叫是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
 

也就述說,使用者自定義的 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();
           }
       }
   }

code 4
 

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中定義了raedObject和writeObject方法,這兩個方法中定義了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被序列化。

 

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

答:可以通過在被序列化的類中增加writeObject 和 readObject方法。

那麼問題又來了,雖然ArrayList中寫了writeObject 和 readObject 方法,但是這兩個方法並沒有顯示的被呼叫啊。

問:如果一個類中包含writeObject 和 readObject 方法,那麼這兩個方法是怎麼被呼叫的呢?

ObjectOutputStream
 

從code 4中,我們可以看出,物件的序列化過程通過ObjectOutputStream和ObjectInputputStream來實現的,那麼帶著剛剛的問題,我們來分析一下ArrayList中的writeObject 和 readObject 方法到底是如何被呼叫的呢?

為了節省篇幅,這裡給出ObjectOutputStream的writeObject的呼叫棧:
 

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 方法,那麼這兩個方法是怎麼被呼叫的?

答:在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法時,會通過反射的方式呼叫。

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

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

Serializable介面的定義:
 

public interface Serializable {
}

如果嘗試對一個未實現Serializable介面的類進行序列化,會丟擲java.io.NotSerializableException。這是為什麼呢?Serializable只是一個空介面,如何實現的呢?

其實這個問題也很好回答,我們再回到剛剛ObjectOutputStream的writeObject的呼叫棧:
 

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、Array和Serializable型別,如果不是則直接丟擲NotSerializableException

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

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

總結
 

1、如果一個類想被序列化,需要實現Serializable介面。否則將丟擲NotSerializableException異常,這是因為,在序列化操作過程中會對型別進行檢查,要求被序列化的類必須屬於Enum、Array和Serializable型別其中的任何一種。

2、在變數宣告前加上該關鍵字,可以阻止該變數被序列化到檔案中。

3、在類中增加writeObject 和 readObject 方法可以實現自定義序列化策略。