1. 程式人生 > >理解Java集合框架裡面的的transient關鍵字

理解Java集合框架裡面的的transient關鍵字

在分析HashMap和ArrayList的原始碼時,我們會發現裡面儲存資料的陣列都是用transient關鍵字修飾的,如下:

HashMap裡面的:

transient Node<K,V>[] table;

ArrayList裡面的:

transient Object[] elementData

既然用transient修飾,那就說明這個陣列是不會被序列化的,那麼同時我們發現了這兩個集合都自定義了獨自的序列化方式:

先看HashMap自定義的序列化的程式碼:

//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自定義的反序列化的程式碼:

//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的資料給寫到流裡面,然後在反序列化的時候重新使用資料把資料結構恢復出來。

那麼問題來了,為什麼他們明明都實現了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種缺陷,所以應該儘可能的根據實際情況重寫序列化方法。