JAVA拾遺 - ArrayList\LinkedList\HashMap源碼解析與閱讀

本文轉載自:
文/absfree(簡書作者)
原文鏈接:http://www.jianshu.com/p/f174d49b391c
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。

本文會從源碼(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個接口:ListRandomAccessCloneableSerializable

官方文檔對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);

我們可以看到,addget等方法都是我們在使用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實例的構建過程,那麼接下來我們來通過ArrayListgetset等方法的源碼來進一步瞭解它的實現原理。

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足夠大;
增加modCountmodCount的作用是判斷在迭代時是否對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中,而後創建一個新結點,這個新結點的數據爲我們傳入的參數eprev指針爲nullnext指針爲f。然後把頭結點指針指向新創建的結點newNode。而後判斷f是否爲null,若爲null,說明之前鏈表中沒有結點,所以last也指向newNode;若f不爲null,則把fprev指針設爲newNode。最後還需要把sizemodCount都加一,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;
}

這個方法的實現很簡單,主需要直接返回firstitem域(當first不爲null時),若firstnull,則拋出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前插入一個新Nodenode(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,但允許多個valuenull)。這個類與HashTable近似等價,區別在於HashMap不是線程安全的並且允許null鍵和null值。由於基於哈希表實現,所以HashMap內部的元素是無序的。HashMap對與getput操作的時間複雜度是常數級別的(在散列均勻的前提下)。對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會佔用更多的存儲空間,但是getput的操作會更快。當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即爲上面提到的maxSizeloadFactorcapacity的乘積)。tableSizeFor方法會根據給定的initialCapacity返回一個值作爲maxSize

基本實現原理

HashMap是基於拉鍊法處理碰撞的散列表的實現,一個存儲整型元素的HashMap的內部存儲結構如下圖所示:

拉鍊法示意圖

我們可以看到,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域爲它的下一個Nodekeyvalue分別爲保存在Node中的鍵和值。

接下來我們看看put方法的源碼:

public V put(K key, V value) {    
  return putVal(hash(key), key, value, false, true);
}

這個方法內部實際上調用了putVal方法來幹活,hash方法會返回給定keyHashMap中的桶號(即key所在NodeNode數組中的索引),實際上hash方法的作用是在keyhashCode方法的基礎上進一步增加哈希值的隨機度。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;
}

以上代碼的工作過程可以總結爲下圖:

putVal方法示意圖

關於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,否則返回nullgetNode方法的源碼如下:

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方法的邏輯便很容易理解了。


以上是我從源碼角度對ArrayListLinkedListHashMap這三種常用數據結構所做的分析,若有不準確或是不清晰的地方,希望大家指出,謝謝大家:)

文/absfree(簡書作者)
原文鏈接:http://www.jianshu.com/p/f174d49b391c
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。

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