Collection和Map的那些常用的類操作的實現原理簡要理解筆記

內容更新中……

Java集合框架
集合類

  • Collection(interface)

    • List(interface)
      • ArrayList:數組實現,適合隨機訪問元素
      • LinkedList(實現了Queue接口):鏈表實現,適合插入、刪除、移動
      • Vector(與ArrayList相比多了個線程安全)
    • Set(interface)

      • HashSet(使用散列函數)——> 通過HashMap實現,add(E e)是通過HashMap的add(K,V)方法實現,E是K;remove(Object o)是通過HashMap的remove(Object o)實現的。按哈希值保存其值,通過Iterator迭代器訪問hashset。
      • TreeSet(使用紅黑樹)——> 通過TreeMap實現。保存的值是排序的。
      • LinkedHashSet(鏈表結合散列函數)。繼承了HashSet,通過LinkedHashMap實現的。值是按照插入的順序存儲的。(結構有HashMap,和LinkList。雙向鏈表存儲插入的Entry元素。)
    • Queue(interface)

  • Map(interface)

    • HashMap(可以存放null)
    • TreeMap(不可以存放null,排序功能)
    • HashTable(不可以存放null,與HashMap比多了個線程安全)

其中線程安全類除了上面說的Vector和HashTable外,還有Stack、Enumeration是線程安全類。
除此集合類外,線程安全的類還有StringBuffer等。

線程安全是指在訪問方法時不允許別的線程去改變,並不是說一系列操作是同步的。
比如StringBuffer的apeend方法線程安全的,但是調用多次append方法可能會對最後的結果造成順序上的差別(append單句是順序的)。

幾個類和接口的比較
Collection:接口,各種集合結構的父接口
Collections:專用靜態類,包含各種有關集合操作的靜態方法。提供一系列靜態方法實現對各種集合的搜索、排序、線程安全化等操作。
Array:Java中最基本的一個存儲結構,提供動態創建和訪問Java數組的方法。元素類型必須相同。效率高,但容量固定無法改變。無法判斷實際多少元素。
Arrays:靜態類。專門用來操作數組,提供搜索、排序、複製等靜態方法。

HashMap的設計:
數據結構中有數組和鏈表來存數據。
數組的優缺點:訪問迅速,插入和刪除難
鏈表的優缺點:插入和刪除簡單,訪問複雜度高
這確實是兩個極端。

如何綜合數組和鏈表的特性,做出訪問容易,插入和刪除都容易的數據結構?
可以,就是哈希表(HashTable):拉鍊法,使用數組,數組裏面是鏈表(鏈表的數組,數組中存的是鏈表的頭結點,就像鄰接表一樣)
這些元素是通過散列公式存進去的。

HashMap也是一個線性的數組實現的,其存儲的容器就是一個線性數組。(如何用線性數組來存取鍵值對呢)
首先HashMap裏面有一個靜態內部類Entry,其重要的屬性有key,value,next,從key,value看出來Entry是HashMap鍵值對實現的一個基礎bean,HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map裏面的內容都保存在Entry[]裏面。
transient Entry[] table;

HashMap的存取實現

//存儲時
int hash = key.hashCode();
int index = hash % Entry[].length;
Entry[hash] = value;
//取值時
int hash = key.hashCode;
int index = hash % Entry[].length;
return Entry[index];

Put操作
如果兩個key通過hash%Entry[].length得到的index相同會不會有覆蓋的風險(兩個不同的key擁有相同的index)。
因爲有個next屬性,作用是指向下一個Entry。如果A的index是0,B的index也是0,那麼進來B的時候,A的next是B。index = 0的位置其實是放了三個鍵值對的。數組中存儲的是最後插入的元素。

public V put(K key, V value){
     if(k == null)
          return putForNullKey(value);
     int hash = hash(key.hashCode());
     int i = indexFor(hash,table.length);
     //遍歷鏈表,看有沒有相同的,就是拉鍊法的那個拉鍊後面的東西
     for(Entry<K,V> e = table[i] ; e != null ; e = e.next){
          Object k;
          if(e.hash == hash && ((k = e.key) == key || key.equals(k)) )//如果key在鏈表中已存在,替換爲新的value{
               V oldValue = e.value;
               e.value = value;
               e.recordAccess(this);
               return oldValue;
          }
     }
     modCount++;
     addEntry(hash,key,value,i);
     return null;
}

void addEntry(int hash ,K key , V value, int buketIndex)
{     
     Entry<K,V> e = table[buketIndex];
     table[bucketIndex] = new Entry<K,V>(hash,key,value,e);//e是Entry.next
     if(size++ >= threshold)
          resize(2*table.length);
}

HashMap裏面包含一些優化方面的實現,比如Entry數組的長度一定後,隨着map裏面數據的越來越長,這樣同一個index的鏈長度就會很長,網HashMap裏面設置一個引子,隨着map的size越來越大,Entry[]會以一定的規則加長長度。

說完HashMap後,從源碼來簡單看看Collection的子接口List接口的實現類ArrayList的添加和刪除元素到底是怎麼操作的

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

直接添加元素就是先判斷是否需要擴容,然後直接把元素添加到末尾

    public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

先看這個添加的函數,在索引index處添加元素E element。
首先是確定index的範圍,然後根據當前的size+1判斷是否需要擴容(擴容機制:看當前的arraylist是否爲空,如果爲空就是取默認容量和傳入參數的較大值,然後調用enSureExplicitCapacity方法進行擴容。
modCount++,然後看傳入的size+1參數是否大於add之前數組 elementData 的大小,如果是,就調用grow方法進行擴容 newCapacity=oldCapacity+(oldCapacity>>1),擴大1.5倍,然後將擴容後的數組複製一份Array.copyof給 elementData )
再者就是使用native方法arraycopy去對數組進行一個複製。其聲明如下:

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

將elementData數組第index個元素後的元素拷到elementData從第index+1位置到後面的位置,長度是size-index。其實做的就是後移操作。

    public E remove(int index) {
        rangeCheck(index);
        modCount++;
        E oldValue = elementData(index);
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,numMoved);
        elementData[--size] = null; // clear to let GC do its work
        return oldValue;
    }

至於這個移除第index個位置的元素操作,依舊先檢查index的範圍,修改次數modCount加1,然後獲取要刪除索引的元素的值。
然後就是數組往前移動覆蓋到index的值,如果移動的長度要大於0,則使用System.arraycopy 的方法去複製數組;然後把數組的最後一個元素置空,讓GC去回收它,返回的是一箇舊的元素的值。

(PS:在一個迭代器初始的時候會賦予它調用這個迭代器的對象的mCount,如何在迭代器遍歷的過程中,一旦發現這個對象的mcount和迭代器中存儲的mcount不一樣那就拋異常。
下面是這個mcount相關機制的解釋引用:
Fail-Fast 機制
我們知道 java.util.HashMap 不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了map,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。這一策略在源碼中的實現是通過 modCount 域,modCount 顧名思義就是修改次數,對HashMap 內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的 expectedModCount。在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 Map:注意到 modCount 聲明爲 volatile,保證線程之間修改的可見性。所以當大家遍歷那些非線程安全的數據結構時,儘量使用迭代器)

由此也可以聯想在遍歷ArrayList的過程中,不能add和remove元素,此時會報錯誤
java.util.ConcurrentModificationException

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

再看一下這個移除object的操作,如果要移除的對象爲空,List是可以添加null的。所以需要遍歷list,然後根據index去查看如果有相同的那麼就用新的數組去覆蓋原來舊的數組。

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

移除元素操作用到了快速移除fastRemove方法,與remove(int index)方法類似,只是不需要檢查index的範圍,因爲這個index已經確定好是存在的。

    public void clear() {
        modCount++;
        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }

clear元素很簡單,全部置空然後長度歸0

    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

往ArrayList中添加實現Collection的類,先轉換成Object數組,然後判斷當前默認值和傳入參數(兩個list的長度和)確定是否需要擴容,再把新的數組a複製到數組的末尾。

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        Object[] a = c.toArray(); // c可以轉換成數組
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,numMoved);
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

在指定位置index中往ArrayList中添加實現Collection的類,先轉換成Object數組,然後判斷當前默認值和傳入參數(兩個list的長度和)確定是否需要擴容,把原list數組index及以後的數據移到index+numNew(要添加數組的長度)處,再把新的數組a複製到數組的index處,再更新新list的size。

public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }

removeAll首先需要判斷要移除的c是否爲空,如果爲空,拋出空指針異常。

removeAll基於batchRemove來實現的,batchRemove源碼如下:

private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

先將當前的數據備份到同步局部變量final數組elementData(這個數組不能指向其它數組對象,內容可以變),設置讀寫兩個指針r,w。遍歷當前list,把當前list中除了要移除的元素之外的元素保存到elementData中。如果沒能遍歷完list(可能c.contains拋出異常),將未能夠遍歷的元素(index爲r之後)全部複製到w指位置後(類似於未能遍歷元素的恢復)。w指針更新位置(w位置和w之前的元素就是新數組的元素)。如果w位置不在list末尾,說明元素改動過了(至少有一次c.contains爲true),此時將w後面多餘的元素刪除置空,讓GC回收它們。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章