在分析HashMap和ArrayList的源碼時,咱們會發現裏面存儲數據的數組都是用transient關鍵字修飾的,以下:java
HashMap裏面的:數組
transient Node<K,V>[] table;
ArrayList裏面的:數據結構
transient Object[] elementData
既然用transient修飾,那就說明這個數組是不會被序列化的,那麼同時咱們發現了這兩個集合都自定義了獨自的序列化方式:app
先看HashMap自定義的序列化的代碼:jvm
//1 private void writeObject(java.io.ObjectOutputStream s) throws IOException { int buckets = capacity(); // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); s.writeInt(buckets); s.writeInt(size); internalWriteEntries(s); } //2 public void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException { Node<K,V>[] tab; if (size > 0 && (tab = table) != null) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { s.writeObject(e.key); s.writeObject(e.value); } } } }
再看HashMap自定義的反序列化的代碼:code
//1 private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { // Read in the threshold (ignored), loadfactor, and any hidden stuff s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException("Illegal load factor: " + loadFactor); s.readInt(); // Read and ignore number of buckets int mappings = s.readInt(); // Read number of mappings (size) if (mappings < 0) throw new InvalidObjectException("Illegal mappings count: " + mappings); else if (mappings > 0) { // (if zero, use defaults) // Size the table using given load factor only if within // range of 0.25...4.0 float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f); float fc = (float)mappings / lf + 1.0f; int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : (fc >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)fc)); float ft = (float)cap * lf; threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? (int)ft : Integer.MAX_VALUE); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node[cap]; table = tab; // Read the keys and values, and put the mappings in the HashMap for (int i = 0; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false, false); } } }
這裏面咱們看到HashMap的源碼裏面自定義了序列化和反序列化的方法,序列化方法主要是把當前HashMap的buckets數量,size和裏面的k,v對一一給寫到了對象輸出流裏面,而後在反序列化的時候,再從流裏面一一的解析出來,而後又從新恢復出了HashMap的整個數據結構。對象
接着咱們看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(); } }
而後反序列化的實現:內存
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(); } } }
ArrayList裏面也是把其size和裏面不爲null的數據給寫到流裏面,而後在反序列化的時候從新使用數據把數據結構恢復出來。ci
那麼問題來了,爲何他們明明都實現了Serializable接口,已經具有了自動序列化的功能,爲啥還要從新實現序列化和反序列化的方法呢?
(1)HashMap中實現序列化和反序列化的緣由:
在HashMap要定義本身的序列化和反序列化實現,有一個重要的因素是由於hashCode方法是用native修飾符修飾的,也就是用它跟jvm的運行環境有關,Object類中的hashCode源碼以下:
public native int hashCode();
也就是說不一樣的jvm虛擬機對於同一個key產生的hashCode多是不同的,因此數據的內存分佈可能不相等了,舉個例子,如今有兩個jvm虛擬機分別是A和B,他們對同一個字符串x產生的hashCode不同:
因此致使:
在A的jvm中它的經過hashCode計算它在table數組中的位置是3
在B的jvm中它的經過hashCode計算它在table數組中的位置是5
這個時候若是咱們在A的jvm中按照默認的序列化方式,那麼位置屬性3就會被寫入到字節流裏面,而後經過B的jvm來反序列化,一樣會把這條數據放在table數組中3的位置,而後咱們在B的jvm中get數據,因爲它對key的hashCode和A不同,因此它會從5的位置取值,這樣以來就會讀取不到數據。
如何解決這個問題,首先致使上面問題的主要緣由在於由於hashCode的不同從而可能致使內存分佈不同,因此只要在序列化的時候把跟hashCode有關的因素好比上面的位置屬性給排除掉,就能夠解決這個問題。
最簡單的辦法就是在A的jvm把數據給序列化進字節流,而不是一刀切把數組給序列化,以後在B的jvm中反序列化時根據數據從新生成table的內存分佈,這樣就來就完美解決了這個問題。
(2)ArrayList中實現序列化和反序列化的緣由:
在ArrayList中,咱們知道數組的長度會隨着數據的插入而不斷的動態擴容,每次擴容都須要增長原數組一半的長度,這而一半的長度極端狀況下都是null值,因此在序列化的時候能夠把這部分數據排除出去,從而節省時間和空間:
for (int i=0; i<size; i++) { s.writeObject(elementData[i]); }
注意ArrayList在序列化的時候用的size來遍歷原數組中的元素,而並非elementData.length也就是數組的長度,而size的大小就是數組裏面非null元素的個數,因此這裏才採用了自定義序列化的方式。
到這裏細心的朋友可能有個疑問:HashMap中也就是採用的動態數組擴容爲何它在序列化的時候用的是table.length而不是size呢,這其實很容易回答在HashMap中table.length必須是2的n次方,並且這個值會決定了好幾個參數的值,因此若是也把null值給去掉,那麼必需要從新的估算table.length的值,有可能形成全部數據的從新分佈,因此最好的辦法就是保持原樣。
注意上面的null值,指的是table裏面Node元素是null,而並非HashMap裏面的key等於null,而key是Node裏面的一個字段。
總結:
本文主要介紹了在HashMap和ArrayList中其核心的數據結構字段爲何用transient修飾並分別介紹了其緣由,因此使用序列化時,應該謹記effective java中的一句話:當一個對象的物理表示方法與它的邏輯數據內容有實質性差異時,使用默認序列化形式有N種缺陷,因此應該儘量的根據實際狀況重寫序列化方法。