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。