1. 程式人生 > >從原始碼角度認識ArrayList,LinkedList與HashMap

從原始碼角度認識ArrayList,LinkedList與HashMap

本文會從原始碼(JDK 1.8)的角度來分析以下幾個Java中常用的資料結構,主要會分析原理與實現,以及每個資料結構所支援的常用操作的複雜度。

ArrayList
LinkedList
HashMap

在對以上資料結構進行具體分析時,我們主要會從以下三個角度來切入:

Why:為什麼要使用這個資料結構?這個資料結構是為解決什麼問題而出現的?
What:這個資料結構的原理與實現是什麼?所支援的各項操作的複雜度如何?
How:如何使用這個資料結構?

ArrayList
定義
快速瞭解ArrayList究竟是什麼的一個好方法就是看JDK原始碼中對ArrayList類的註釋,大致翻譯如下:
/**
  * 實現了List的介面的可調整大小的陣列。實現了所有可選列表操作,並且允許所有型別的元素,
  * 包括null。除了實現了List介面,這個類還提供了去動態改變內部用於儲存集合元素的陣列尺寸
  * 的方法。(這個類與Vector類大致相同,除了ArrayList是非執行緒安全外。)size,isEmpty,
  * get,set,iterator,和listIterator方法均為常數時間複雜度。add方法的攤還時間複雜度為
  * 常數級別,這意味著,新增n個元素需要的時間為O(n)。所有其他方法的時間複雜度都是線性級別的。
  * 常數因子要比LinkedList低。
  * 每個ArrayList例項都有一個capacity。capacity是用於儲存ArrayList的元素的內部陣列的大小。
  * 它通常至少和ArrayList的大小一樣大。當元素被新增到ArrayList時,它的capacity會自動增長。
  * 在向一個ArrayList中新增大量元素前,可以使用ensureCapacity方法來增加ArrayList的容量。
  * 使用這個方法來一次性地使ArrayList內部陣列的尺寸增長到我們需要的大小提升效能。需要注意的
  * 是,這個ArrayList實現是未經同步的。若在多執行緒環境下併發訪問一個ArrayList例項,並且至少
  * 一個執行緒對其作了結構型修改,那麼必須在外部做同步。(結構性修改指的是任何新增或刪除了一個或
  * 多個元素的操作,以及顯式改變內部陣列尺寸的操作。set操作不是結構性修改)在外部做同步通常通
  * 過在一些自然地封裝了ArrayList的物件上做同步來實現。如果不存在這樣的物件,ArrayList應
  * 使用Collections.synchronizedList方法來包裝。最好在建立時就這麼做,以防止對ArrayList
  * 無意的未同步訪問。(List list = Collections.synchronizedList(new ArrayList(...));)
  * ArrayList類的iterator()方法以及listIterator()方法返回的迭代器是fail-fast的:
  * 在iterator被建立後的任何時候,若對list進行了結構性修改(以任何除了通過迭代器自己的
  * remove方法或add方法的方式),迭代器會丟擲一個ConcurrentModificationException異常。
   * 因此,在遇到併發修改時,迭代器馬上丟擲異常,而不是冒著以後可能在不確定的時間發生不確定行為
  * 的風險繼續。需要注意的是,迭代器的fail-fast行為是不能得到保證的,因為通常來說在未同步併發
  * 修改面前無法做任何保證。fail-fast迭代器會盡力丟擲ConcurrentModificationException異常。
  * 因此,編寫正確性依賴於這個異常的程式是不對的:fail-fast行為應該僅僅在檢測bugs時被使用。
  * ArrayList類是Java集合框架中的一員。
*/
根據原始碼中的註釋,我們瞭解了ArrayList用來組織一系列同類型的資料物件,支援對資料物件的順序迭代與隨機訪問。我們還了解了ArrayList所支援的操作以及各項操作的時間複雜度。接下來我們來看看這個類實現了哪些介面。
public class ArrayList<E> extends AbstractList<E>      
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
我們可以看到,它實現了4個介面:List、RandomAccess、Cloneable、Serializable。官方文件對List介面的說明如下:List是一個有序的集合型別(也被稱作序列)。使用List介面可以精確控制每個元素被插入的位置,並且可以通過元素在列表中的索引來訪問它。列表允許重複的元素,並且在允許null元素的情況下也允許多個null元素。List介面定義了以下方法:
ListIterator<E> listIterator();
void add(int i, E element);
E remove(int i);
E get(int i);
E set(int i, E element);
int indexOf(Object element);
我們可以看到,add、get等方法都是我們在使用ArrayList時經常用到的。在ArrayList的原始碼註釋中提到了,ArrayList使用Object陣列來儲存集合元素。我們來一起看下它的原始碼中定義的如下幾個欄位:
/** * 預設初始capacity. */
private static final int DEFAULT_CAPACITY = 10;
/** * 供空的ArrayList例項使用的空的陣列例項 */
private static final Object[] EMPTY_ELEMENTDATA = {};
/** * 供預設大小的空的ArrayList例項使用的空的陣列例項。
     * 我們把它和EMPTY_ELEMENTDATA區分開來,一邊指導當地一個元素被新增時把內部陣列尺寸設為
     * 多少
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/** * 存放ArrayList中的元素的內部陣列。
     * ArrayList的capacity就是這個內部陣列的大小。
     * 任何elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList在第一個元素
     * 被新增進來時,其capacity都會被擴大至DEFAULT_CAPACITYhe
*/
transient Object[] elementData; // non-private to simplify nested class access
/** *ArrayList所包含的元素數 */
private int size;
通過以上欄位,我們驗證了ArrayList內部確實使用一個Object陣列來儲存集合元素。那麼接下來我們看一下ArrayList都有哪些構造器,從而瞭解ArrayList的構造過程。
ArrayList的構造器
首先我們來看一下我們平時經常使用的ArrayList的無參構造器的原始碼:
/** * Constructs an empty list with an initial capacity of ten. */
public ArrayList() {   
  this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
我們可以看到,無參構造器僅僅是把ArrayList例項的elementData欄位賦值為DEFAULTCAPACITY_EMPTY_ELEMENTDATA。接下來,我們再來看一下ArrayList的其他構造器:
/** * Constructs an empty list with the specified initial capacity.
     * * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
*/
public ArrayList(int initialCapacity) {   
  if (initialCapacity > 0) {       
    this.elementData = new Object[initialCapacity];   
  } else if (initialCapacity == 0) {       
    this.elementData = EMPTY_ELEMENTDATA;   
  } else {       
    throw new IllegalArgumentException("Illegal Capacity: "+                                          
        initialCapacity);   
  }
}

/** * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's * iterator.
     * * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {   
  elementData = c.toArray();   
  if ((size = elementData.length) != 0) {       
    // c.toArray might (incorrectly) not return Object[] (see 6260652)       
    if (elementData.getClass() != Object[].class)           
      elementData = Arrays.copyOf(elementData, size, Object[].class);     
  } else {       
    // replace with empty array.       
    this.elementData = EMPTY_ELEMENTDATA;   
  }
}
通過原始碼我們可以看到,第一個構造器指定了ArrayList的初始capacity,然後根據這個初始capacity建立一個相應大小的Object陣列。若initialCapacity為0,則將elementData賦值為EMPTY_ELEMENTDATA;若initialCapacity為負數,則丟擲一個IllegalArgumentException異常。
第二個構造器則指定一個Collection物件作為引數,從而構造一個含有指定集合物件元素的ArrayList物件。這個構造器首先把elementData例項域賦值為集合物件轉為的陣列,而後再判斷傳入的集合物件是否不含有任何元素,若是的話,則將elementData賦值為EMPTY_ELEMENTDATA;若傳入的集合物件至少包含一個元素,則進一步判斷c.toArray方法是否正確返回了Object陣列,若不是的話,則需要用Arrays.copyOf方法把elementData的元素型別改變為Object。
現在,我們又瞭解了ArrayList例項的構建過程,那麼接下來我們來通過ArrayList的get、set等方法的原始碼來進一步瞭解它的實現原理。
add方法原始碼分析
/** * Appends the specified element to the end of this list.
     * * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by

{@link Collection#add})
*/
public boolean add(E e) {   
  ensureCapacityInternal(size + 1);  // Increments modCount!!   
  elementData[size++] = e;   
  return true;
}
我們可以看到,在add方法內部,首先呼叫了ensureCapacityInternal(size+1),這句的作用有兩個:

保證當前ArrayList例項的capacity足夠大;
增加modCount,modCount的作用是判斷在迭代時是否對ArrayList進行了結構性修改。

然後通過將內部陣列下一個索引處的元素設定為給定引數來完成了向ArrayList中新增元素,返回true表示新增成功。
get方法原始碼分析
/** * Returns the element at the specified position in this list.
     * * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {   
  rangeCheck(index);   
  return elementData(index);
}
首先呼叫了rangeCheck方法來檢查我們傳入的index是否在合法範圍內,然後呼叫了elementData方法,這個方法的原始碼如下:
E elementData(int index) {   
  return (E) elementData[index];
}
set方法原始碼分析
/** * Replaces the element at the specified position in this list with
     * the specified element.
     * * @param index index of the element to replace
     * @param element element to be stored at the specified position
    * @return the element previously at the specified position
    * @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E set(int index, E element) {   
  rangeCheck(index);   
  E oldValue = elementData(index);   
  elementData[index] = element;   
  return oldValue;
}
我們可以看到,set方法的實現也很簡單,首先檢查給定的索引是否在合法範圍內,若在,則先把該索引處原來的元素儲存在oldValue中,然後把新元素放到該索引處並返回oldValue即可。
LinkedList
定義
LinkedList類原始碼中的註釋如下:
/** * 實現了List介面的雙向連結串列。實現了所有可選列表操作,並且可以儲存所有型別的元素,包括null。
  * 對LinkedList指定索引處的訪問需要順序遍歷整個連結串列,直到到達指定元素。
  * 注意LinkedList是非同步的。若多執行緒併發訪問LinkedList物件,並且至少一個執行緒對其做
  * 結構性修改,則必須在外部對它進行同步。這通常通過在一些自然封裝了LinkedList的物件上
  * 同步來實現。若不存在這樣的物件,這個list應使用Collections.synchronizedList來包裝。    
  * 這最好在建立時完成,以避免意外的非同步訪問。
  * LinkedList類的iterator()方法以及listIterator()方法返回的迭代器是fail-fast的:
  * 在iterator被建立後的任何時候,若對list進行了結構性修改(以任何除了通過迭代器自己的
  * remove方法或add方法的方式),迭代器會丟擲一個ConcurrentModificationException異常。
  * 因此,在遇到併發修改時,迭代器馬上丟擲異常,而不是冒著以後可能在不確定的時間發生不確定行為
  * 的風險繼續。需要注意的是,迭代器的fail-fast行為是不能得到保證的,因為通常來說在未同步併發
  * 修改面前無法做任何保證。fail-fast迭代器會盡力丟擲ConcurrentModificationException異常。
  * 因此,編寫正確性依賴於這個異常的程式是不對的:fail-fast行為應該僅僅在檢測bugs時被使用。
  * LinkedList類是Java集合框架中的一員。
*/
LinkedList是對連結串列這種資料結構的實現(對連結串列還不太熟悉的小夥伴可以參考深入理解資料結構之連結串列),當我們需要一種支援高效刪除/新增元素的資料結構時,可以考慮使用連結串列。總的來說,連結串列具有以下兩個優點:

插入及刪除操作的時間複雜度為O(1)
可以動態改變大小

連結串列主要的缺點是:由於其鏈式儲存的特性,連結串列不具備良好的空間區域性性,也就是說,連結串列是一種快取不友好的資料結構。
支援的操作
LinkedList主要支援以下操作:
void addFirst(E element);
void addLast(E element);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
boolean add(E e) //把元素e新增到連結串列末尾
void add(int index, E element) //在指定索引處新增元素
以上操作除了add(int index, E element)外,時間複雜度均為O(1),而add(int index, E element)的時間複雜度為O(N)。
Node類
在LinkedList類中我們能看到以下幾個欄位:
transient int size = 0;
/** * 指向頭結點  */
transient Node<E> first;
/** * 指向尾結點 */
transient Node<E> last;
我們看到,LinkedList只儲存了頭尾節點的引用作為其例項域,接下來我們看一下LinkedList的內部類Node的原始碼如下:
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物件的next域指向它的下一個結點,prev域指向它的上一個結點,item為本結點所儲存的資料物件。
addFirst原始碼分析
/** * Inserts the specified element at the beginning of this list.
      * * @param e the element to add
*/
public void addFirst(E e) {   
  linkFirst(e);
}
實際幹活的是linkFirst,它的原始碼如下:
/** * Links e as first element. */
private void linkFirst(E e) {   
  final Node<E> f = first;   
  final Node<E> newNode = new Node<>(null, e, f);   
  first = newNode;   
  if (f == null)       
    last = newNode;   
  else       
    f.prev = newNode;   
  size++;   
  modCount++;
}
首先把頭結點引用存於變數f中,而後建立一個新結點,這個新結點的資料為我們傳入的引數e,prev指標為null,next指標為f。然後把頭結點指標指向新建立的結點newNode。而後判斷f是否為null,若為null,說明之前連結串列中沒有結點,所以last也指向newNode;若f不為null,則把f的prev指標設為newNode。最後還需要把size和modCount都加一,modCount的作用與在ArrayList中的相同。
getFirst方法原始碼分析
/** * Returns the first element in this list.
     * * @return the first element in this list
     * @throws NoSuchElementException if this list is empty
*/
public E getFirst() {   
  final Node<E> f = first;   
  if (f == null)       
    throw new NoSuchElementException();   
  return f.item;
}
這個方法的實現很簡單,主需要直接返回first的item域(當first不為null時),若first為null,則丟擲NoSuchElementException異常。
removeFirst方法原始碼分析
/** * Removes and returns the first element from this list.
     * * @return the first element from this list
     * @throws NoSuchElementException if this list is empty
*/
public E removeFirst() {   
  final Node<E> f = first;   
  if (f == null)       
    throw new NoSuchElementException();   
  return unlinkFirst(f);
}
unlinkFirst方法的原始碼如下:
/** * Unlinks non-null first node f. */
private E unlinkFirst(Node<E> f) {   
  // assert f == first && f != null;   
  final E element = f.item;   
  final Node<E> next = f.next;   
  f.item = null;   
  f.next = null; // help GC   
  first = next;   
  if (next == null)       
    last = null;   
  else       
    next.prev = null;   
  size--;   
  modCount++;   
  return element;
}
add(int index, E e)方法原始碼分析
/** * Inserts the specified element at the specified position in this list.
     * Shifts the element currently at that position (if any) and any
     * subsequent elements to the right (adds one to their indices).
     * * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {   
  checkPositionIndex(index);   
  if (index == size)       
    linkLast(element);   
  else       
    linkBefore(element, node(index));
}
這個方法中,首先呼叫checkPositionIndex方法檢查給定index是否在合法範圍內。然後若index等於size,這說明要在連結串列尾插入元素,直接呼叫linkLast方法,這個方法的實現與之前介紹的linkFirst類似;若index小於size,則呼叫linkBefore方法,在index處的Node前插入一個新Node(node(index)會返回index處的Node)。linkBefore方法的原始碼如下:
/** * Inserts element e before non-null Node succ. */
void linkBefore(E e, Node<E> succ) {   
  // assert succ != null;   
  final Node<E> pred = succ.prev;   
  final Node<E> newNode = new Node<>(pred, e, succ);   
  succ.prev = newNode;   
  if (pred == null)       
    first = newNode;   
  else       
    pred.next = newNode;   
  size++;   
  modCount++;
}
我們可以看到,在知道要在哪個結點前插入一個新結點時,插入操作是很容易的,時間複雜度也只有O(1)。下面我們來看一下node方法是如何獲取指定索引處的Node的:
/** * Returns the (non-null) Node at the specified element index. */
Node<E> node(int index) {   
  // assert isElementIndex(index);   
  if (index < (size >> 1)) {       
    Node<E> x = first;       
    for (int i = 0; i < index; i++)           
      x = x.next;       
    return x;   
  } else {       
    Node<E> x = last;       
    for (int i = size - 1; i > index; i--)           
      x = x.prev;       
    return x;   
  }
}
首先判斷index位於連結串列的前半部分還是後半部分,若是前半部分,則從頭結點開始遍歷,否則從尾結點開始遍歷,這樣可以提升效率。我們可以看到,這個方法的時間複雜度為O(N)。
HashMap
Map介面
我們先來看下它的定義:
一個把鍵對映到值的物件被稱作一個對映表物件。對映表不能包含重複的鍵,每個鍵至多可以與一個值關聯。Map介面提供了三個集合檢視:鍵的集合檢視、值的集合檢視以及鍵值對的集合檢視。一個對映表的順序取決於它的集合檢視的迭代器返回元素的順序。一些Map介面的具體實現(比如TreeMap)保證元素有一定的順序,其它一些實現(比如HashMap)則不保證元素在其內部有序。
也就是說,Map介面定義了一個類似於“字典”的規範,讓我們能夠根據鍵快速檢索到它所關聯的值。我們先來看看Map介面定義了哪些方法:
void clear()
boolean containsKey(Object key) //判斷是否包含指定鍵
boolean containsValue(Object value) //判斷是否包含指定值
boolean isEmpty()
V get(Object key) //返回指定鍵對映的值
V put(K key, V value) //放入指定的鍵值對
V remove(Object key)
int size()
Set<Map.Entry<K,V>> entrySet()
Set<K> keySet()
Collection<V> values()
HashMap的定義
HashMap<K, V>是基於雜湊表這個資料結構的Map介面具體實現,允許null鍵和null值(最多隻允許一個key為null,但允許多個value為null)。這個類與HashTable近似等價,區別在於HashMap不是執行緒安全的並且允許null鍵和null值。由於基於雜湊表實現,所以HashMap內部的元素是無序的。HashMap對與get與put操作的時間複雜度是常數級別的(在雜湊均勻的前提下)。對HashMap的集合檢視進行迭代所需時間與HashMap的capacity(bucket的數量)加上HashMap的尺寸(鍵值對的數量)成正比。因此,若迭代操作的效能很重要,不要把初始capacity設的過高(不要把load factor設的過低)。
 (對散列表(雜湊表)這種資料結構還不太熟悉的小夥伴請戳這裡散列表的原理與實現)有兩個因素會影響一個HashMap的效能:intial capacity(初始容量)和load factor(負載因子)。intial capacity就是HashMap物件剛建立時其內部的雜湊表的“桶”的數量。load factor等於maxSize / capacity,也就是HashMap所允許的最大鍵值對數與桶數的比值。增大load factor可以節省空間但查詢一個元素的時間會增加,減小load factor會佔用更多的儲存空間,但是get與put的操作會更快。當HashMap中的鍵值對數量超過了maxSize(即load factor與capacity的乘積),它會再雜湊,再雜湊會重建內部資料結構,桶數(capacity)大約會增加到原來的兩倍。HashMap預設的load factor大小為0.75,這個數值在時間與空間上做了很好的權衡。當我們清楚自己將要大概存放多少資料時,也可以自定義load factor的大小。HashMap的常用方法如下:
void clear()
boolean containsKey(Object key)
boolean containsValue(Object value)
V get(Object key)
V put(K key, V value)
boolean isEmpty()
V remove(Object key)
int size()
Collection<V> values()
Set<Map.Entry<K,V>> entrySet()
Set<K> keySet()
HashMap的構造器
HashMap有以下幾個構造器:
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
HashMap(Map<? extends K,? extends V> m) //建立一個新的HashMap,用m的資料填充
無參構造器的原始碼如下:
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */
public HashMap() {   
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
這個構造器把loadFactor域設為DEFAULT_LOAD_FACTOR(0.75),其他域都保持預設值。
我們再來看下第三個構造器的原始碼:
/** * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     * * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {   
  if (initialCapacity < 0)       
    throw new IllegalArgumentException("Illegal initial capacity: " +                                           initialCapacity);   
  if (initialCapacity > MAXIMUM_CAPACITY)       
    initialCapacity = MAXIMUM_CAPACITY;   
  if (loadFactor <= 0 || Float.isNaN(loadFactor))       
    throw new IllegalArgumentException("Illegal load factor: " +                                           loadFactor);   
  this.loadFactor = loadFactor;   
  this.threshold = tableSizeFor(initialCapacity);
}
以上原始碼中的threshold即為上面提到的maxSize(loadFactor與capacity的乘積)。tableSizeFor方法會根據給定的initialCapacity返回一個值作為maxSize。
基本實現原理
HashMap是基於拉鍊法處理碰撞的散列表的實現,一個儲存整型元素的HashMap的內部儲存結構如下圖所示:

linked.jpg
我們可以看到,HashMap是採用陣列+連結串列實現的,在JDK 1.8中,對HashMap做了進一步優化,引入了紅黑樹。當連結串列的長度大於8時,就會使用紅黑樹來代替連結串列。
put方法原始碼分析
在分析put方法前,我們先來看下HashMap的如下欄位:
/** * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
table欄位是一個Node<K, V>陣列,這個陣列由連結串列的頭結點組成。我們再來看一下Node<K, V>的定義:
static class Node<K,V> implements Map.Entry<K,V> {   
  final int hash;   
  final K key;   
  V value;   
  Node<K,V> next;   
  Node(int hash, K key, V value, Node<K,V> next) {       
    this.hash = hash; //"桶號",即該Node在陣列的索引      
    this.key = key;       
    this.value = value;       
    this.next = next;   
  }   
  public final K getKey() {
    return key;
  }   
  public final V getValue() {  
    return value;
  }   
  public final String toString() {
    return key + "=" + value;
  }   
  public final int hashCode() {       
    return Objects.hashCode(key) ^ Objects.hashCode(value);   
  }   
  public final V setValue(V newValue) {       
    V oldValue = value;       
    value = newValue;       
    return oldValue;   
  }   
  . . .
}
Node類的hash域為它在Node陣列中的索引,next域為它的下一個Node,key、value分別為儲存在Node中的鍵和值。接下來我們看看put方法的原始碼:
public V put(K key, V value) {   
  return putVal(hash(key), key, value, false, true);
}
這個方法內部實際上呼叫了putVal方法來幹活,hash方法會返回給定key在HashMap中的桶號(即key所在Node在Node陣列中的索引),實際上hash方法的作用是在key的hashCode方法的基礎上進一步增加雜湊值的隨機度。putVal方法的原始碼如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {   
  Node<K,V>[] tab;
  Node<K,V> p;
  int n, i;   
  //若table為空或table的length為0則需要通過resize方法擴容
  if ((tab = table) == null || (n = tab.length) == 0)       
    n = (tab = resize()).length;
  //讓傳入的hash與n-1做與運算從而得到目標Node的索引
  //若該索引處為null,則直接插入包含了key-value pair的new Node  
  if ((p = tab[i = (n - 1) & hash]) == null)       
    tab[i] = newNode(hash, key, value, null);   
  else {       
    //若索引處不為null,則判斷key是否存在
    Node<K,V> e;
    K k;       
    //若key存在,則直接覆蓋value
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    //若key不存在,則判斷table[i]是否為TreeNode      
    else if (p instanceof TreeNode)           
      //若是的話,說明此處為紅黑樹,直接插入key-value pair
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
      //否則遍歷連結串列       
      else {           
        for (int binCount = 0; ; ++binCount) {               
          if ((e = p.next) == null) {                   
            p.next = newNode(hash, key, value, null);     
            //連結串列長度大於8則轉為紅黑樹             
            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                       
              treeifyBin(tab, hash);                   
            break;               
          }
          //若key已經存在則直接覆蓋value               
          if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))                   
            break;               
          p = e;           
       }       
     }       
     if (e != null) { // existing mapping for key           
       V oldValue = e.value;           
       if (!onlyIfAbsent || oldValue == null)               
         e.value = value;           
       afterNodeAccess(e);           
       return oldValue;       
    }   
  }   
  ++modCount;   
  //若超過maxSize,則擴容
  if (++size > threshold)       
    resize();   
  afterNodeInsertion(evict);   
  return null;
}
以上程式碼的工作過程可以總結為下圖:

put.png
關於HashMap我們還需要知道它的擴容方法resize的時間消耗比較大,因此我們在能夠估計到大致需要儲存的資料量時,應該為其指定一個合適的初始容量。
get方法原始碼分析
public V get(Object key) {   
  Node<K,V> e;   
  return (e = getNode(hash(key), key)) == null ? null : e.value;
}
我們可以看到這個方法內部呼叫了getNode方法以獲取key所在的Node,若成功獲取到了,則返回key對應的value,否則返回null。getNode方法的原始碼如下:
final Node<K,V> getNode(int hash, Object key) {   
  Node<K,V>[] tab;
  Node<K,V> first, e;
  int n;
  K k;   
  //若table不為空且長度大於0且指定索引處Node不為空
  //則進一步進行其他判斷,否則直接返回null
  if ((tab = table) != null && (n = tab.length) > 0
    && (first = tab[(n - 1) & hash]) != null) {
      //指定索引處的Node即為我們要找的Node,直接返回即可       
      if (first.hash == hash && // always check first node           
          ((k = first.key) == key || (key != null && key.equals(k))))           
        return first;
      //我們的目標Node和first處於同一紅黑樹或同一連結串列中,
      //位於first之後       
      if ((e = first.next) != null) {           
        //first為紅黑樹
        if (first instanceof TreeNode)               
          return ((TreeNode<K,V>)first).getTreeNode(hash, key);             
        //first為連結串列
        do {               
          if (e.hash == hash &&                   
             ((k = e.key) == key || (key != null && key.equals(k))))                   
            return e;           
        } while ((e = e.next) != null);       
      }   
    }   
    return null;
}
理解了putVal方法,getNode方法的邏輯便很容易理解了。
以上是我從原始碼角度對ArrayList,LinkedList,HashMap這三種常用資料結構所做的分析,若有不準確或是不清晰的地方,希望大家指出,謝謝大家:)
參考資料

Java Docs
Java 8之重新認識HashMap

相關推薦

原始碼角度認識ArrayListLinkedListHashMap

本文會從原始碼(JDK 1.8)的角度來分析以下幾個Java中常用的資料結構,主要會分析原理與實現,以及每個資料結構所支援的常用操作的複雜度。 ArrayList LinkedList HashMap 在對以上資料結構進行具體分析時,我們主要會從以下三個角度來切入: Wh

java中List介面的實現類 ArrayListLinkedListVector 的區別 list實現類原始碼分析

java面試中經常被問到list常用的類以及內部實現機制,平時開發也經常用到list集合類,因此做一個原始碼級別的分析和比較之間的差異。 首先看一下List介面的的繼承關係: list介面繼承Col

原始碼角度分析ViewStub 疑問原理

一、提出疑問     ViewStub比較簡單,之前文章都提及到《Android 效能優化 三 佈局優化ViewStub標籤的使用》,但是在使用過程中有一個疑惑,到底是ViewStub上設定的引數有效還是在其包括的layout中設定引數有效?如果不明白描述的問題,可以看下以下佈局虛擬碼。 res/lay

[cocos2d-x]原始碼角度思考convertToWorldSpace()convertToWorldSpaceAR()座標系的轉換

convertToWorldSpace() 話不多說,先上原始碼,之後再慢慢講解: (5和6圖截圖的時候重複了,這裡就不弄出來了) 只要通過圖1到圖8中我寫的註釋進行分析(不懂的地方可

Android View 繪製流程 invalidate 和postInvalidate 分析--原始碼角度

整個View樹的繪製流程是在ViewRootImpl.java類的performTraversals()函式展開的,該函式做的執行過程可簡單概況為  根據之前設置的狀態,判斷是否需要重新計算檢視大小(measure)、是否重新需要佈局檢視的位置(layout

MySql輕鬆入門系列————第一站 原始碼角度輕鬆認識mysql整體框架圖

一:背景 1. 講故事 最近看各大技術社群,不管是知乎,掘金,部落格園,csdn基本上看不到有小夥伴分享sqlserver類的文章,看樣子這些年sqlserver沒落了,已經後繼無人了,再寫sqlserver是不可能再寫了,這輩子都不會寫了,只能靠技術輸出mysql維持生活這樣子。 二:瞭解架構圖 mysql

面試角度分析ArrayList原始碼

> 注:本系列文章中用到的jdk版本均為`java8` `ArrayList`類圖如下: ![](https://img2020.cnblogs.com/blog/1719198/202012/1719198-20201215105542094-324269707.png) `ArrayList`的底

java集合【12】——— ArrayListLinkedListVector的相同點區別是什麼?

[TOC] 要想回答這個問題,可以先把各種都講特性,然後再從底層儲存結構,執行緒安全,預設大小,擴容機制,迭代器,增刪改查效率這幾個方向入手。 ## 特性列舉 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/2021030602125

ArrayListLinkedListVector的對比

最佳實踐 都是 更多 訪問 blog AR 一個數 ali alt 1. List概述 List,就如圖名字所示一樣,是元素的有序列表。當我們討論List時,將其與Set作對比是一個很好的辦法,Set集合中的元素是無序且唯一的。下圖是Collection的類繼承圖,從圖中你

JDK類集框架實驗(ArrayListLinkedListTreeSetHashSetTreeMapHashMap

        ArrayList import java.util.ArrayList; public class C8_3 { public static void main(String[] args) { //

原始碼角度看Spring生命週期(官方最全)

Spring在beanfactory中給出了spring的生命週期的list列表 一、bean初始化前的處理 Bean factory implementations should support the standard bean lifecycle interfaces as

JVM系列第4講:原始碼到機器碼發生了什麼?

在上篇文章我們聊到,無論什麼語言寫的程式碼,其到最後都是通過機器碼執行的,無一例外。那麼對於 Java 語言來說,其從原始碼到機器碼,這中間到底發生了什麼呢?這就是今天我們要聊的。 如下圖所示,編譯器可以分為:前端編譯器、JIT 編譯器和AOT編譯器。下面我們逐個講解。 前端編譯器:原始碼到位元組碼

原始碼角度解析 - ScrollView巢狀ListView只顯示一行的問題

<ScrollView android:id="@+id/scroll_view" android:layout_width="match_parent" android:layout_height="match_parent">

原始碼角度解析 - ScrollView巢狀ViewPager不顯示的問題

<ScrollView android:id="@+id/scroll_view" android:layout_width="match_parent" android:layout_height="match_parent">

原始碼角度深入理解OKHttp3

在日常開發中網路請求是很常見的功能。OkHttp作為Android開發中最常用的網路請求框架,在Android開發中我們經常結合retrofit一起使用,俗話說得好:“知其然知其所以然”,所以這篇文章我們通過原始碼來深入理解OKHttp3(基於3.12版本) 常規使用 在瞭

原始碼角度理解Java設計模式--責任鏈模式

本文內容思維導圖如下:                                        

帶你原始碼角度分析ViewGroup中事件分發流程

序言 這篇博文不是對事件分發機制全面的介紹,只是從原始碼的角度分析ACTION_DOWN、ACTION_MOVE、ACTION_UP事件在ViewGroup中的分發邏輯,瞭解各個事件在ViewGroup的分發邏輯對理解、解決滑動衝突問題很有幫助。 ViewGroup中事件分發流

原始碼角度深入理解Retrofit2

Retrofit2作為目前最火的網路請求框架之一,它是一個由Square 組織開發的可以在Android和java中使用的安全型HTTP客戶端(官方文件描述“Type-safe HTTP client for Android and Java by Square”)。本文將從Retrofit2簡單使用入

原始碼角度理解Java設計模式——裝飾者模式

一、飾器者模式介紹 裝飾者模式定義:在不改變原有物件的基礎上附加功能,相比生成子類更靈活。 適用場景:動態的給一個物件新增或者撤銷功能。 優點:可以不改變原有物件的情況下動態擴充套件功能,可以使擴充套件的多個功能按想要的順序執行,以實現不同效果。 缺點:更多的類,使程式複雜 型別:結構型。 類圖

原始碼角度理解Java設計模式——門面模式

一、門面模式介紹 門面模式定義:也叫外觀模式,定義了一個訪問子系統的介面,除了這個介面以外,不允許其他訪問子系統的行為發生。 適用場景:子系統很複雜時,增加一個介面供外部訪問。 優點:簡化層級間的呼叫,減少依賴,防止風險。 缺點:如果設計不當,增加新的子系統可能需要修改門面類的原始碼,違背了開閉原則