Java集合類,從原始碼解析底層實現原理

xjanting發表於2018-09-29

總體框架

Java集合總體框架及主要介面,抽象類分析

ArrayList底層實現和原理

Vector底層實現和原理

LinkedList底層實現和原理(也是queue的實現)

ConcurrentLinkedQueue底層實現和原理(常用於併發程式設計)

HashSet底層實現(是由HashMap實現)和原理

TreeSet底層實現(是由TreeMap實現)和原理

HashMap底層實現和原理(1.7陣列+連結串列,1.8陣列+連結串列+紅黑樹)

ConcurrentHashMap底層實現和原理(常用於併發程式設計)

TreeMap底層實現和原理

LinkedHashMap底層實現和原理

hash演算法:一致性hash演算法原理及java實現

下面對上面的文章做一下總結,一些在上面文章中沒有涉及到的點,在詳細的說明一下。

Set和Map的關係

Set代表一種無序不可重複的集合,Map代表一種由多個Key-Value對組成的集合。表面上看它們之間似乎沒有啥關係,但是Map可以看成是Set的擴充套件。為什麼這麼說呢?看下面的這個例子:

在Map的方法中有一個這樣的方法,Set<k> keySet() ,也就是說Map中的鍵可以轉化成一個Set集合。如果把value看成key的一個附屬品,或者把key-value看成是一個整體,那麼Map集合就變成了一個Set集合。

HashSet和HashMap的關係

HashSet和HashMap有很多的相似之處,對於HashSet而言,採用了Hash演算法來決定元素的儲存位置,HashMap而言,將value當成了key的附屬品,根據Key的Hash值來決定存放的位置。

有一點需要說明一下,經常聽說,集合儲存的是物件, 這其實是不準確的。準確來說,集合中儲存的其實是物件的引用地址或者稱為引用變數。而引用地址或者引用變數指向了實際的java物件。java集合實際是引用變數的集合而非java物件的集合。

通過之前的原始碼解析其實可以發現,HashMap在存放key-value時,並沒有過多的考慮value的內容。只是根據key來確定key-value對在陣列中應該存放的位置。HashMap的底層是一個Entry[]陣列,key-value組成了一個entry。當需要向HashMap中新增元素時,首先根據key的hashcode來確定在陣列中存放的位置,如果key為null,採用特殊方法進行處理,存放在陣列的0號位置。如果當前位置已經有元素存在,則遍歷單連結串列,如果兩個key相等,則用新值替換掉舊值,如果key不相等,則插入到連結串列中。有一點需要說明,在jdk8之前,hashmap使用陣列+單連結串列儲存,在8後,採用了陣列+連結串列+紅黑樹儲存。

對於HashSet要說的沒有太多,HashSet的實現也是比較的簡單,它的底層使用HashMap實現的,只是封裝了一個HashMap物件來儲存所有的集合物件。

TreeSet和TreeMap的關係

TreeSet底層採用了一個NavigableMap來儲存TreeSet集合的元素,但實際上NavigableMap只是一個藉口,因為底層依然是使用TreeMap來包含Set集合中的元素。 與HashSet類似,TreeSet也是呼叫TreeMap的方法來實現一些操作。TreeMap的底層是使用“紅黑樹”的排序二叉樹來儲存Map中的每個Entry.關於TreeMap的實現在上面的連結中有詳細的解釋,請自行查閱。

HashSet和HashMap是無序的,而TreeSet和TreeMap是有序的

ArrayList和LinkedList的關係

List代表的是一種線性結構,ArrayList則是一種順序儲存的線性表,ArrayList底層採用陣列來儲存每個元素,LinkedList是一種鏈式儲存的線性表,本質是一個雙向連結串列。

迭代器Iterator

fast-fail快速失敗機制

在迭代的過程中,如果刪除了某一個元素,collection會丟擲ConcurrentModificationException異常。

為什麼會出現這個異常呢?
這是因為在迭代時,某個執行緒對該collection在結構上進行了更改,從而產生fail-fast.當方法檢測到物件修改後,但是不允許這種修改就會丟擲該異常。fail-fast只是一種異常檢測機制,JDK並不能保證該機制一定會發生。

通過一個demo來詳細的說明下:

LinkedList<String> list = new LinkedList<String>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");

for (String a : list) {
    System.out.println(a);
    list.remove(2);
}

執行上面的程式碼便會丟擲 java.util.ConcurrentModificationException;
來看一下LinkedList remove()方法的原始碼:

//刪除方法
 public E remove(int index) {
        checkElementIndex(index); //驗證index是否合法
        return unlink(node(index)); //呼叫unlink方法
    }
 E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;   //modCount+1 敲黑板劃重點
        return element;
    }

關於上面程式碼的具體含義請自行查閱上面的文章。 上面程式碼在刪除指定位置的元素後將執行私有內部類ListItr中的next()方法,進行下一個元素的遍歷。

//在私有內部類ListItr中有如下的屬性定義,再進行遍歷時,將遍歷物件的modCount值賦值給了expectedModCount。
private int expectedModCount = modCount;

public E next() {
     checkForComodification();
     if (!hasNext())
         throw new NoSuchElementException();

      lastReturned = next;
      next = next.next;
      nextIndex++;
      return lastReturned.item;
}
final void checkForComodification() {
       if (modCount != expectedModCount)
              throw new ConcurrentModificationException();
 }

執行next()方法後,會先執行checkForComodification()方法,判斷modCount與expectedModCount是否相等,不相等則丟擲異常。

因為是遍歷物件單方面改變的modCount值,ListItr並沒有監測到,所以變造成了modCount和expectedModCount不相等的情況。於是出現了異常。我的理解是,在使用迭代器進行物件遍歷時,建立了一個新的引用,而新引用指向了遍歷的物件,同時將遍歷物件的一些屬性賦值給了迭代器物件。呼叫遍歷物件的方法時,物件的屬性發生變化,而迭代器物件中的遍歷物件的拷貝唯有進行更新,導致了值得不匹配,從而丟擲異常。這只是我的個人理解,歡迎深入交流。

採用下面的方法就不會出現該異常,是因為迭代器物件進行了屬性的更新! 通過Iterator的方法刪除後,保證了modCount與expectedModCount值的統一。

Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            String str = iterator.next();
            if(str.equals("a")){
                iterator.remove();
            }
        }

 

相關文章