1. 程式人生 > >JDK原始碼學習-ArrayList、LinkedList、HashMap

JDK原始碼學習-ArrayList、LinkedList、HashMap

  ArrayList、LinkedList、HashMap是Java開發中非常常見的資料型別。它們的區別也非常明顯的,在Java中也非常具有代表性。在Java中,常見的資料結構是:陣列、連結串列,其他資料結構基本就是這兩者的組合。

  複習一下陣列、連結串列的特徵。

  陣列:在記憶體中連續的地址塊,查詢按照下標來定址,查詢快速。但是插入元素和刪除元素慢,需要移動元素。

  連結串列:記憶體中邏輯上可以連線到一起的一組節點。每個節點除了儲存本身,還儲存了下一個元素的地址。查詢元素需要依次找找各個元素,查詢慢,插入和刪除元素只需要修改元素指向即可。

結合這兩種資料結構的特徵,就不難理解ArrayList、LinkedList、HashMap的各種操作了。

ArrayList

陣列

ArrayList的底層實現就是陣列,根據陣列的特徵就很好理解ArrayList的各個特性了。

下面是ArrayList中最基本的兩個變數:儲存物件的陣列和陣列大小。

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

在執行add操作前,會首先檢查陣列大小是否足以容納新的元素,如果不夠,就進行擴容,擴容的公式是:新的陣列大小=(老的陣列大小*3)/2 + 1,例如初始時陣列大小為10,第一次擴容後,陣列大小就為16,再擴容一次變為25。

Fail-Fast 機制

在操作元素的方法中,例如add方法和remove方法中,會看到modCount++操作。這個modCount變數是記錄什麼的?

檢視modCount的定義,modCount是在AbstractList中定義的,其說明如下:

    /**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     *
     * <p>This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     */
    protected transient int modCount = 0;

 modCount記錄是List的結構變化次數,就是List大小變化的次數,如果在遍歷List的時候,發現modCount發生變化,則丟擲異常ConcurrentModificationException。

例如下面的程式碼,定義了一個Array List,向其中增加元素,然後遍歷元素,在遍歷元素過程中,刪除了一個元素。

public class ArrayListRemoveTest {
  public static void main(String[] args) {
    List<String> lstString = new ArrayList<String>();
    lstString.add("hello");

    Iterator<String> iterator = lstString.iterator();
    while (iterator.hasNext()) {
      String item = iterator.next();
      if (item.equals("hello")) {
        lstString.remove(item);
      }
    }
  }
}

 執行後會丟擲異常:

根據報錯堆疊,next方法會呼叫checkForComodification方法,在checkForComodification方法中丟擲異常。

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

程式碼中會比較當前的modCount和expectedModCount的值,expectedModCount的值是在執行Iterator<String> iterator = lstString.iterator();時,在Itr的建構函式中賦值的,是原始的List結構變化次數。在執行remove方法後,List的大小發生了變化,則modCount發生了變化,兩次modCount不同,丟擲異常。做這個檢查的原因,是要保持單執行緒的唯一操作。這就是Fail-Fast機制。

LinkedList

連結串列

LinkedList的底層實現就是連結串列,插入和刪除只需要改變節點指向,效率高。隨機訪問需要依次找到各個節點,慢。

LinkedList在類中包含了 first 和 last 兩個指標(Node)。Node 中包含了上一個節點和下一個節點的引用,這樣就構成了雙向的連結串列。

    transient int size = 0;
    transient Node<E> first; //連結串列的頭指標
    transient Node<E> last; //尾指標
    //儲存物件的結構 Node, LinkedList的內部類
    private static class Node<E> {
        E item;
        Node<E> next; // 指向下一個節點
        Node<E> prev; //指向上一個節點

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

在新增節點時,只需要建立一個Node,指向這個Node即可。刪除節點,修改上一個節點的prev指向即可。

HashMap

陣列+連結串列

  HashMap是Java資料結構中兩大結構陣列和連結串列的組合。其結構圖如下:

 可以看出,HashMap底層是陣列,陣列中的每一項又是一個連結串列。

  當程式試圖將一個key-value對放入HashMap中時,程式首先根據該 key 的 hashCode() 返回值決定該 Entry 在陣列中的儲存位置,即陣列下標。如果陣列該位置上沒有元素,就直接將該元素放到此陣列中的該位置上。如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的儲存位置相同(即碰撞)。再呼叫equals,如果這兩個 Entry 的 key 通過 equals 比較返回 true,新新增 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋,就是value替換。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新新增的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新新增的 Entry 位於 Entry 鏈的頭部。

  簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 物件。HashMap 底層採用一個 Entry[] 陣列來儲存所有的 key-value 對,當需要儲存一個 Entry 物件時,會根據 hash 演算法來決定其在陣列中的儲存位置,再根據 equals 方法決定其在該陣列位置上的連結串列中的儲存位置;當需要取出一個Entry 時,也會根據 hash 演算法找到其在陣列中的儲存位置,再根據 equals 方法從該位置上的連結串列中取出該Entry。