1. 程式人生 > >[jjzhu學java]之JDK集合框架原始碼分析

[jjzhu學java]之JDK集合框架原始碼分析

Java Collection

aa

圖中實線邊框表示的是實現類(ArrayList, Hashtable等),虛線邊框的是抽象類(AbstractCollection,AbstractSequentialListd等),而點線邊框的是介面(Collection, Iterator, List等)。
從圖中可以發現,所有集合類都實線了Iterator介面,下面是Iterator介面的原始碼

public interface Iterable<T> {
    Iterator<T> iterator();
}

Iterator中提供的iterator方法是用於遍歷集合中元素的,各個具體實現類可以按照自己的方式來實現對自身元素的遍歷。具體的實現會在後面章節進行分析。
Iterator介面原始碼為:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

從Iterator原始碼中可以看到,其提供了對當前集合的三種操作:判斷集合是否有下一個元素、獲取下一個元素、移除元素。

Collection介面

Collection介面繼承了Iterator介面,並添加了其他一些方法,如isEmpty()判斷集合是否為空,add(E e)新增元素等方法,其各方法如下所示:

方法名 功能
int size() 返回集合大小
boolean isEmpty() 判斷是否為空
boolean contains(Object o) 判斷元素o是否在集合中
Iterator iterator() 返回集合的迭代器
Object[] toArray() 將元素轉化為陣列
boolean add(E e) 新增元素
boolean remove(Object o) 將元素O移除
void clear() 將集合內元素全部移除

AbstractCollection類

AbstractCollection類實現了Collection介面中的部分方法,並且多提供了一個抽象方法:

public abstract Iterator<E> iterator();

由其他繼承AbstractCollection類的類自己去實現自己想要的iterator()。因為不同型別的集合(如List,Set、Map之間的實現是不同的,無法提供統一的實現方法,這裡抽象出來由它們自己定義自己的iterator)
這裡只解讀部分方法的實現
- contains(Object o)判斷集合中是否包含物件O

public boolean contains(Object o) {
    Iterator<E> e = iterator();
    if (o==null) {
        while (e.hasNext())
        if (e.next()==null)
            return true;
    } else {
        while (e.hasNext())
        if (o.equals(e.next()))
            return true;
    }
    return false;
    }

閱讀原始碼可以知道,AbstractCollection類是先獲取集合的迭代器iterator,然後對集合中的元素進行遍歷,逐個比較每個元素與物件o是否相等,若在遍歷過程中存在於o相等的物件則返回true,否則返回false,所以該方法的時間複雜度為O(n),n為集合中所包含的元素。
其他方法像remove(Object o),toArray(),containsAll(Collection

AbstractList類

AbstractList類則是對所有List類做了抽象,封裝了List中一些常用的操作方法。

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> 
方法名 功能 是否實現
add(E e) 新增元素
get(int index) 根據索引獲取元素 抽象方法
set(int index, E element) 設定指定索引位置的值 ×
remove(int index)

AbstractList類繼承了AbstractCollection類並實現了List介面,AbstractList類中有兩個內部類Itr和ListItr,兩個不同的迭代器。

從原始碼中可以看到,其對AbstractCollection的抽象方法public abstract Iterator iterator()的實現如下所示:

public Iterator<E> iterator() {
    return new Itr();
}

直接new Itr()並返回,在看看其是如何實現自己的Iterator的。
Itr類繼承了Iterator介面

private class Itr implements Iterator<E> {
    /**
     * 記錄的遍歷元素的索引遊標
     */
    int cursor = 0;

    /**
     * 記錄最近一次被返回的元素的索引。若該元素進行了remove操作,則設定為-1
     * 
     */
    int lastRet = -1;

    /**
     * modCount記錄的是對當前列表改變的次數
     */
    int expectedModCount = modCount;

    public boolean hasNext() {//實現是否遍歷完
        return cursor != size();//當前遍歷的元素索引是否=集合長度
    }

    public E next() {//獲取下一個元素
        checkForComodification();
        try {
            E next = get(cursor);//獲取當前遊標所指向的元素
            lastRet = cursor++;//將當前遊標索引賦值給lastRet,並將遊標後移
            return next;//返回當前元素
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }

    public void remove() {
        if (lastRet == -1)//lastRet為-1時,表示當前元素已經執行過remove操作
            throw new IllegalStateException();
        checkForComodification();

        try {
            AbstractList.this.remove(lastRet);
            /*
            *如果lastRet比cursor小,也就是說lastRet記錄的索引對應的元素
            *已經遍歷過,cursor則需要後移一位,因為remove操作會使陣列的
            *長度-1,後面的元素會向前移動一位,所以cursor也需要相應的
            *向前移動一位
            */
            if (lastRet < cursor)
                cursor--;
            lastRet = -1;//標記為-1
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException e) {
            throw new ConcurrentModificationException();
        }
    }

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

Itr類中定義了一個索引遊標cursor,以及lastRet變數,cursor的作用是記錄list中元素遍歷的索引,每呼叫next方法一次,cursor後移一位,並將lastRet變數指向當前元素的索引,而remove操作會將lastRet賦值為-1。這個迭代器是針對線性列表的迭代器。而且在Itr類中nexth和remove操作都要呼叫checkForComodification方法,目的是判斷在遍歷的過程中,檢查當前list有沒有被改變(從後面具體類的實現,如Vector、ArrayList的實現中可以看到,他們進行新增或移除元素的時候,會改變modCount的值),一旦發生了變化,modCount 必然和expectedModCount不相等,就會丟擲ConcurrentModificationException異常。

Vector類

Vector類可以動態增長和收縮的儲存物件,Vector類繼承了AbstractList類並實現了List介面,支援隨機訪問,可以被克隆,也可以被序列化。類的繼承關係如下:

public class Vector<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

vector類與繼承類的關係圖如下圖所示:

image

Vector類用了一個Object[] elementData陣列來儲存元素資料,用elementCount
記錄Vector中元素的個數。

//一個數組來儲存資料
protected Object[] elementData;
protected int elementCount;

Vector提供了四組構造方法,也就是說可以有四種方式初始化一個Vector

public Vector() {
    this(10);
}
public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement)
public Vector(Collection<? extends E> c)

建構函式中initialCapacity可以指定Vector的初始容量,capacityIncrement可以指定Vector在容量已滿時,每次增長的容量大小。也可以通過給定初始集合來初始化Vector。再看看Vector類中是如何對元素進行新增、刪除、更新操作是如何進行的。
- 新增元素

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

vector中對元素的新增的步驟為:

1.先判斷vector容量是否容納的下新新增的元素。

  private void ensureCapacityHelper(int minCapacity) {
  int oldCapacity = elementData.length;
  if (minCapacity > oldCapacity) {
      Object[] oldData = elementData;
       /*新的容量是根據初始化capacityIncrement的值,
         *來進行更新的,設定了大於0的值,就原來的容量
         *再加上設定的增長容量作為新的容量,否知直接擴
         *容為當前容量的兩倍
         */
      int newCapacity = (capacityIncrement > 0) ?
              (oldCapacity + capacityIncrement) : (oldCapacity * 2);
              if (newCapacity < minCapacity) {
                  newCapacity = minCapacity;
              }
              elementData = Arrays.copyOf(elementData, newCapacity);
  }
}

2.然後再在尾部新增該元素,元素個數+1
這裡我們可以看到,add方法是被==synchronized==關鍵字修飾的,而被synchronized關鍵字修飾的方法是執行緒安全的,只允許一個執行緒執行add方法,可以防止在多執行緒的情況下,多個執行緒同時對vector做修改。
- 刪除元素

public synchronized E remove(int index) {
    modCount++;
    if (index >= elementCount) //索引越界
        throw new ArrayIndexOutOfBoundsException(index);
    Object oldValue = elementData[index];
    //判斷是否移動(若index不是最後一個元素索引,則需要將索引在index後
    //的元素向前移動一位)
    int numMoved = elementCount - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                 numMoved);
    elementData[--elementCount] = null; //會被GC,不存在記憶體洩露
    //返回被移除的元素
    return (E)oldValue;
    }

刪除過程是將索引號在index之後的元素全部向前移動一位,然後元素個數-1,
並返回被刪除的元素。(刪除操作也是執行緒安全的。)

vector還提供了其他一些方法,這裡就不一一解讀,下表vector提供的方法做了總結

方法名 功能 返回值 原理
copyInto(Object[] anArray) 將Vector內的元素複製到給定陣列中 呼叫System.arraycopy()函式
lastIndexOf(Object o) 返回給定物件的最後的索引 若o在vector中,則返回最後的索引值,否知返回-1 遍歷vecotr中的元素

小節
vector內部實現使用一個Object陣列儲存元素的,可以動態的擴容,支援隨機訪問,根據索引獲取元素的時間複雜度為O(1),而新增元素和刪除元素操作的時間複雜度都為O(n),其中n為vector中所儲存的元素個數。

Stack(棧)

Stack類繼承了Vector類,其實現也是用陣列儲存元素,但是對元素的訪問和插入做了一定的限制,只能在陣列尾部新增元素,也只能在陣列尾部刪除元素,所以Stack的一個特性就是後進先出(LIFO)。下表中給出了Stack類中提供的方法:

方法名 功能 返回值
E push(E item) 元素進棧操作 進棧的元素
E pop() 元素出棧) 出棧的元
boolean empty() 棧是否為空 為空時true,否者false
int search(Object o) 找出給定元素的索引 給定元素的索引,未找到為-1

ArrayList

ArrayList和Vector的功能都類似,唯一一點的不同的是,對比ArrayList和Vector各方法的實現,Vector的方法都是同步的(方法前都加了Synchronized關鍵字修飾),是執行緒安全的(thread-safe),而ArrayList則不是。由於Vector加了執行緒同步,所以會影響效能,因此,ArrayList的效能表現的會比Vector的好。另外,ArrayList和Vector的動態擴容機制也不同,Vector可以設定capacityIncrement來設定每次擴容時擴容的小,而ArrayList的擴容機制可以在ensureCapacity方法中可以看出

int newCapacity = (oldCapacity * 3)/2 + 1;

每一次擴容都是擴大為原容量大小的1.5倍。

AbstractSequentialList

這個類提供了一個基本的List介面實現,為實現序列訪問的資料儲存結構的提供了所需要的最小化的介面實現。對於支援隨機訪問資料的List比如陣列,應該優先使用AbstractList。

這裡類是AbstractList類中與隨機訪問類相對的另一套系統,採用的是在迭代器的基礎上實現的get、set、add和remove方法。

為了實現這個列表。僅僅需要拓展這個類,並且提供ListIterator和size方法。
對於不可修改的List,程式設計人員只需要實現Iterator的hasNext、next和hasPrevious、previous和index方法
對於可修改的List還需要額外實現Iterator的的set的方法
對於大小可變的List,還需要額外的實現Iterator的remove和add方法

AbstractSequentiaList和其他RandomAccess的List的主要的區別是AbstractSequentiaList的主要方法都是通過迭代器實現的。而不是直接實現的(不通過迭代器,比如ArrayList實現的方式是通過陣列實現的。)因此對於一些可以提供RandomAccess的List方法,直接使用方法可能更快。因為迭代器移動需要一定代價。
其提供了一個抽象的迭代器方法:

public abstract ListIterator<E> listIterator(int index);

需要繼承AbstractSequentiaList類的類自己去實現,
而ListIterator介面的定義如下所示:

public interface ListIterator<E> extends Iterator<E>

ListIterator介面繼承了Iterator介面,除了Iterator
介面提供的hastNext,next,remove方法之外,還新增瞭如下功能的方法:

方法名 功能
boolean hasPrevious() 如果逆序遍歷List還有下一個元素則返回true,否則返回false
E previous() 返回當前元素的前一個元素值
int nextIndex() 返回當前元素後一個元素的索引
int previousIndex() 返回當前元素前一個元素的索引
void set(E e) 設定當前索引的值
void add(E e) 插入值

ListIterator介面提供了對List的順序和逆序遍歷,在迭代的過程中執行修改列表中的值(set和add操作)

LinkedList(線性連結串列)

LinkedList類的定義如下:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

有LinkedList類的定義可以看出,LinkedList實現了List介面,能對它進行List的常用操作,實現了Deque介面,則將其當作雙端佇列使用,也實現了Cloneable介面,覆蓋了clone()函式,可被克隆,也實現了java.io.Serializable介面,表明LinkedList可以被序列化,用於網路傳輸。
LinkedList的資料結構是基於雙向迴圈連結串列的,切頭結點不存放資料
image

LinkedList中定義了一個Entry header的頭結點和int size的連結串列大小兩個屬性

//頭結點
private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;//連結串列大小

而Entry是LinkedList的內部類,該類定義了雙向迴圈連結串列

private static class Entry<E> {
    E element;//儲存當前節點的元素
    Entry<E> next;//指向下一個節點
    Entry<E> previous;//指向前一個節點

    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
    }
}

LinkedList插入雙向迴圈連結串列的插入操作如圖所示:
image

LinkedList中提供了連結串列插入元素的操作

public boolean add(E e) {
    addBefore(e, header);
    return true;
}

其呼叫了向header前插入元素e的addBefore方法,addBefore方法就實現了向指定節點之前插入節點的操作:

private Entry<E> addBefore(E e, Entry<E> entry) {
    Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
    newEntry.previous.next = newEntry;//改變指標
    newEntry.next.previous = newEntry;
    size++;
    modCount++;
    return newEntry;
}

還有addFirst(E e)和addLast(E e)方法提供了向header節點前後插入新節點的操作。
LinedList刪除操作的指標變化如下圖所示
image
LinkedList迭代器

public ListIterator<E> listIterator(int index) {
    return new ListItr(index);
}

LinkedList中提供了listIterator(int index)方法返回迭代器以遍歷List,其中index引數指定了迭代器遍歷的起始索引位置。
從程式碼中可以看到,其直接new 了一個ListItr並返回,而ListItr類是LinkedList的內部私有類,ListItr類的定義如下:

private class ListItr implements ListIterator<E>

實現了ListIterator中提供的add、remove、set等操作。
ListItr類中有四個屬性:

private Entry<E> lastReturned = header; //最後一次返回的節點,預設header
private Entry<E> next;//下一個要返回的節點
private int nextIndex;//下一個返回節點的索引
private int expectedModCount = modCount;//記錄當前LinkedList中被改變的次數

這裡需要解釋一下的是expectedModCount的作用,expectedModCount的值是在初始化LinkedList就指定為modCount,expectedModCount是用來判斷在遍歷過程中,是否有非LinedList中的方法對當前列表進行了操作(add,remove等操作都會改變modCount值),因為LinkedList是執行緒不安全的,索引,ListItr會判斷expectedModCount與modCount的值是否相等,所以ListItr方法中很多方法中都加上checkForComodification()的方法,而checkForComodification()方法就是用來檢查在遍歷期間,List是否有被其他地方操作過,改變了ListItr的結構,一發現,迭代器就會丟擲ConcurrentModificationException的異常。
現在看ListItr的建構函式

ListItr(int index) {
    if (index < 0 || index > size)
        throw new IndexOutOfBoundsException("Index: "+index+
                ", Size: "+size);
    if (index < (size >> 1)) {
        next = header.next;
        for (nextIndex=0; nextIndex<index; nextIndex++)
            next = next.next;
    } else {
        next = header;
        for (nextIndex=size; nextIndex>index; nextIndex--)
            next = next.previous;
    }
}

要找到指定的索引節點,他這裡用了一個小技巧,用了一個if判斷語句

size >> 1 //size/2

如果index < size >> 1,說明起始位置在列表的前半部分,可以從前往後,否則就在後半部分,就可以從後往前找,這樣就節省了一半的時間。
而ListItr類中的其他方法(add, remove, set等操作)與LinkedList中相關的操作都類似,這裡就不累述了。
LinkedList(jdk 1.6以後的版本)還提供了一個descendingIterator

public Iterator<E> descendingIterator() {
    return new DescendingIterator();
}

該迭代器可以逆序遍歷LinkedList。

Map介面

Map介面中給定了一些通用的函式模板

header 1 header 2
int size() 返回鍵值對的個數
boolean isEmpty() 判斷map是否為空
boolean containsKey(Object key) 判斷是否包含指定主鍵Key
boolean containsValue(Object value) 判斷是否包含指定的值value
V get(Object key) 根據給定key獲取value
V put(K key, V value) 向map中新增鍵值對
V remove(Object key) 根據主鍵移除對應的鍵值對
void putAll(Map m) 新增給定map集合
void clear() 清空map中的鍵值對
Set keySet() 返回包含所有key的集合
Collection values() 返回map中所有的value集合
Set

AbstractMap

AbstractMap抽象類的定義如下:

public abstract class AbstractMap<K,V> implements Map<K,V>

AbstractMap抽象類繼承了Map介面中,並實現了一些骨幹方法。
AbstractMap類中只有一個抽象方法

public abstract Set<Entry<K,V>> entrySet();

也就是說,要實現一個自定義的map只需要實現entrySet方法就可以了,但是隻能實現一個無法被改變的Map類,因為在AbstractMap中對put方法是預設不支援的。

public V put(K key, V value) {
    throw new UnsupportedOperationException();
}

所以,要實現一個可以有對Map中的元素進行改動的自定義類,還需要實現put方法,以及entrySet().iterator()返回的迭代器的remove()方法。
AbstractMap中有兩個屬性:

transient volatile Set<K>        keySet = null;
transient volatile Collection<V> values = null;

兩個屬性都由transient和volatile關鍵詞修飾。transient表明這兩個屬性在序列化時是不會被序列化的,volatile表明兩個屬性對所有程序是可見的,而且值有變化的時候,會通知所有程序。

HashMap

HashMap繼承了AbstractMap抽象類,實現了Map介面,可以被克隆及序列化。其定義如下:

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

HashMap中提供了三個建構函式:
- HashMap():構造一個具有預設初始容量 (16) 和預設載入因子 (0.75) 的空 HashMap
- HashMap(int initialCapacity):構造一個帶指定初始容量和預設載入因子 (0.75) 的空 HashMap
- HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和負載因子的空 HashMap

初始容量是指雜湊表中桶的數量
載入因子是指雜湊表中可用的散列表空間,當散列表的負載因子超過了載入因子,散列表將自動擴容。(因為散列表被填的越滿,衝突發生的就越頻繁,查詢效率和插入效率會降低)
- 儲存結構

HashMap是通過雜湊表實現的,存取速度非常快,因為雜湊表中會有衝突,而HashMap是怎麼實現雜湊和碰撞避免的呢。其實Java對HashMap是一個“連結串列雜湊”,也就是用“拉鍊法”來解決碰撞的。其資料結構如下圖所示:

image

可以很直觀的看出,java中是用一個數組來實現的,而陣列中的每一項是一個連結串列,從原始碼中也可以看出,用一個了table陣列,而陣列的元素是Entry型別的。

transient Entry[] table;//不被序列化

Entry是HashMap中的內部類,繼承了Map類中的Entry,其定義如下所示:

static class Entry<K,V> implements Map.Entry<K,V>

Entry中定義了四個屬性

final K key;
V value;
Entry<K,V> next;
final int hash;

其中next屬性就是用來將hash到相同位置的(即發生碰撞)鍵值對鏈在一起的。可以在其建構函式看出,在碰撞鏈,HashMap是採用插表頭法實現的。

Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}
  • 雜湊位置的確定

    在HashMap中,不論是put、get還是remove操作,第一步要做的就是定位當鍵值對在雜湊表對應的位置,HashMap的內部儲存結構是陣列+連結串列實現的,在插入元素的時候,HashMap裡面的元素位置儘量分佈均勻,儘量使得每個位置上的元素數量只有一個,那麼當用hash演算法求得這個位置的時候,可以馬上定位元素,無需遍歷連結串列,大大優化了查詢的效率。而HashMap中確定元素在散列表中的位置進行了兩步操作:

int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);

第一步是根據key的hashCode呼叫hash函式獲取hash值,然後根據indexFor來獲取在雜湊表中的索引位置。下面在看下hash函式和indexFor是如何實現的

static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
    return h & (length-1);
}

可以看出,hash函式中對h進行了無符號左移和異或運算,indexFor函式是根據hash
值與雜湊表長度進行了取模運算。對於任意給定的物件,只要它的hashCode()返回值
相同,那麼程式呼叫hash函式所計算得到的Hash碼值總是相同的。我們首先想到的就
是把hash值對陣列長度取模運算,這樣一來,元素的分佈相對來說是比較均勻的。但
是,模運算的消耗還是比較大的,在HashMap中是這樣做的:通過indexFor方法來計算
該物件應該儲存在table陣列的哪個索引處。這個方法非常巧妙,它通過h &
(table.length-1)來得到該物件的儲存位,而HashMap底層陣列的長度總是2的n次方,
這是HashMap在速度上的優化。當length總是2的n次方時,h& (length-1)運算等價於對
length取模,也就是h%length,但是&比%具有更高的效率。所以這就是為什麼在初始化
的是要兩容量轉化為2的n次方。

  • HashMap的put方法

    之前提過想要通過繼承AbstractMap是實現一個可以改變的map,實現put方法,以及entrySet().iterator()返回的迭代器的remove()方法。下面是put方法的執行過程:

    image

    可以看HashMap對put方法是如何實現的。

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());//獲取key在hash表中的位置
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

可以知道,在put操作的時候,先判斷key是否為null,因為HashMap對key為null的做了特殊處理。直接儲存在索引位置為0的位置。若key不為null,則先獲取要put的key在雜湊表中的索引位置,然後判斷該位置是否已經有值(是否發生碰撞e!=null),如果有值(發生了碰撞),就遍歷該索引位置的連結串列,再查詢該key值是否已經存在,如果存在,則更新value,否者跳出迴圈,呼叫addEntry方法將當前key-value插入到雜湊表中,完成put操作。
再檢視addEntry方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];//獲取當前位置之前的值
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);//新建並更新
    if (size++ >= threshold)
        resize(2 * table.length);
}

addEntry方法通過獲取給定bucketIndex中原有的值(null也無所謂),然後新建一個Entry更新到bucketIndex位置。HashMap認為,當table中的元素達到一定程度,也就是之前說的負載因子達到一定程度時,碰撞發生的機率會增大,HashMap會自動擴容。

size++ >= threshold

在分析HashMap的擴容機制之前,看看size、threshold屬性的作用。

static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient int size;
int threshold;
final float loadFactor;

從上述屬性可以看到,HashMap預設的初始容量是16,最大允許擴容到的最大容量是2^30,預設的載入因子是0.75,size用來記錄hashMap中儲存的key-value對個數,threshold是根據載入因子*容量得到的,loadFactor是用來記錄使用者自定義的載入因子。HashMap允許在初始化時自定義容量和載入因子.

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);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity];
    init();
}

從上述建構函式可以看到,可以指定一個大於0的initialCapacity,作為初始容量,和一個非NaN大於0的載入因子,但HashMap對自定義的初始容量做了一個處理,處理成了離initialCapacity最近的大於initialCapacity的是2的n次方的一個數作為初始容量。

int capacity = 1;
while (capacity < initialCapacity)
    capacity <<= 1;//2^n
  • HashMap的擴容機制

擴容(resize)就是重新計算容量,向HashMap物件裡不停的新增元素,而HashMap物件內
部的陣列無法裝載更多的元素時,物件就更大的陣列,以便能裝入更多的元素。然而
Java裡的陣列是無法自動擴容的,方法是使用一個新的陣列代替已有的容量小的陣列,
將原有的元素移到新的雜湊表中去。
我們可以分析resize的原始碼

void resize(int newCapacity) {//給定新的容量值
    Entry[] oldTable = table;//新建一個數組
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {//達到最大容量的時候
        threshold = Integer.MAX_VALUE;//則無上限的向表中插入元素
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);//呼叫該transfer函式
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

在擴容操作中,呼叫了transfer方法,其作用就是將舊的陣列中的元素複製到新的陣列中去。

void transfer(Entry[] newTable) {
    Entry[] src = table;//src引用舊的陣列
    int newCapacity = newTable.length;//新的陣列容量
    for (int j = 0; j < src.length; j++) {//遍歷就得陣列中的元素
        Entry<K,V> e = src[j];//儲存當前索引下的Entry
        if (e != null) {
            src[j] = null;//釋放記憶體,讓gc垃圾回收
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);//重新計算位置
                e.next = newTable[i];//插表頭
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

進過了上訴的do{}while()迴圈對連結串列進行遍歷後,因為是插表頭法構建新的鏈,原先的連結串列順序會被反轉。下面舉例演示HashMap的擴容過程。假設其中的雜湊桶陣列table的size=2, 所以key = 3、7、5,put順序依次為 5、7、3。在mod 2以後都衝突在table[1]這裡了。這裡假設負載因子 loadFactor=1,即當鍵值對的實際大小size 大於 table的實際大小時進行擴容。
image
- 執行緒安全

原始碼中可以看出HashMap是執行緒不安全的,所以在多執行緒的環境下,應該儘量避免使用執行緒不安全的HashMap,可以使用執行緒安全的ConcurrentHashMap。
- 小結

(1)擴容是一個特別耗效能的操作,所以當程式設計師在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。

(2) 負載因子是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況非常特殊。

(3) HashMap是執行緒不安全的,不要在併發的環境中同時操作HashMap,建議使用ConcurrentHashMap。

LinkedHashMap

LinkedHashMap繼承了HashMap,並多了一個雙重連結串列的結構。HashMap和LinkedHashMap最大的區別就是HashMap的元素是無序存放的,而LinkedHashMap的元素是有序存放的。
- LinkedHashMap的資料結構

LinkedHashMap的資料結構是散列表+迴圈雙重連結串列,如下圖所示
image
(圖中的資料僅僅代表節點插入的順序)

所以LinkedHashMap維護的雙向連結串列,可以讓元素有序的輸出。LinkedHashMap沒有重寫HashMap的put方法,只是需改了put方法中呼叫的addEntry方法,用來構建雙向迴圈連結串列

void addEntry(int hash, K key, V value, int bucketIndex) {
        createEntry(hash, key, value, bucketIndex);
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        } else {
            if (size >= threshold)
                resize(2 * table.length);
        }
    }
  • LinkedHashMap的使用示例
public static void main(String[] args) {
    //預設按插入的順序輸出
    LinkedHashMap<Integer, String> lhm  = new LinkedHashMap<Integer, String>();
    lhm.put(1, "Lucy");
    lhm.put(12, "Fic");
    lhm.put(23, "Nick");
    System.out.println("----linked hash map output----");
    PrintHelper.showMap(lhm);
    HashMap<Integer, String> hm  = new HashMap<Integer, String>();
    hm.put(1, "Lucy");
    hm.put(12, "Fic");
    hm.put(23, "Nick");
    System.out.println("----hash map output----");
    PrintHelper.showMap(hm);

}

執行程式,可以得到如下輸出:

----linked hash map output----
key:1 value:Lucy
key:12 value:Fic
key:23 value:Nick
----hash map output----
key:1 value:Lucy
key:23 value:Nick
key:12 value:Fic

可以看出LinkedHashMap和HashMap輸出的順序是不同的,LinkedHashMap是按照插入的順序輸出的,而HashMap是無序輸出的。

private transient Entry<K,V> header;

treeMap

  • 定義
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

TreeMapde 的繼承關係如下圖所示:

image

由圖中以很直觀的看出:

TreeMap 是一個有序的key-value集合,它是通過紅黑樹實現的。

TreeMap 繼承於AbstractMap,所以它是一個Map,即一個key-value集合。

TreeMap 實現了NavigableMap介面,意味著它支援一系列的導航方法。比如返回有序的key集合。

TreeMap 實現了Cloneable介面,意味著它能被克隆。

TreeMap 實現了java.io.Serializable介面,意味著它支援序列化。

TreeMap基於紅黑樹(Red-Black tree)實現。該對映根據其鍵的自然順序進行排序,
或者根據建立對映時提供的 Comparator 進行排序,具體取決於使用的構造方法。

TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n)。所以TreeMap的查詢效率非常高
- 紅黑樹

紅黑樹又稱紅-黑二叉樹,它首先是一顆二叉樹,它具體二叉樹所有的特性。同時紅黑樹更是一顆自平衡的排序二叉樹。
紅黑樹顧名思義就是節點是紅色或者黑色的平衡二叉樹,它通過顏色的約束來維持著二叉樹的平衡。對於一棵有效的紅黑樹二叉樹而言我們必須增加如下規則:

1、每個節點都只能是紅色或者黑色

2、根節點是黑色

3、每個葉節點(NIL節點,空節點)是黑色的。

4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。

5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

下圖是一顆典型的紅黑樹

image
對於紅黑樹的操作有:左旋、右旋、著色。
1. 左旋操作

image
image
2. 右旋操作

image
image

我們的重點放在java TreeMap的具體實現,對於紅黑樹的操作細節就不做詳解了。

private final Comparator<? super K> comparator;//比較器
private transient Entry<K,V> root = null;//根節點
private transient int size = 0;//節點數
private transient int modCount = 0;//改變次數
private static final boolean RED   = false;//紅節點
private static final boolean BLACK = true;//黑節點

root節點儲存了當前紅黑樹的根節點。是一個Entry型別的屬性。Entry類是TreeMap的內部類,定義了節點的資料結構,其定義如下:

static final class Entry<K,V> implements Map.Entry<K,V>

聲明瞭左孩子、右孩子、父節點、節點顏色、以及儲存的key-value對

K key;//鍵
V value;//值
Entry<K,V> left = null;//左孩子
Entry<K,V> right = null;//右孩子
Entry<K,V> parent;//父節點
boolean color = BLACK;//節點顏色,預設為黑
  • put方法

TreeMap中put方法的流程如下圖所示

image

從流程圖中可以看出,put方法先判斷整個樹是否為空(root==null),若為空樹,這直接將節點作為根節點插入,否者判斷是否定義了比較器,若未定義比較器,這預設用key的比較器,這種情況下不允許key為null,否者會丟擲NullPointException。然後根據比較器在樹中找出是否存在key,若存在則覆蓋原來的value,否者找到要插入的父節點,然後插入,在做調整(左旋、右旋、著色操作),使得樹重新平衡。
下面是put方法實現的原始碼

public V put(K key, V value) {
    Entry<K,V> t = root;//獲取root節點
    if (t == null) {//判斷是否為空
        //若為空,直接建立節點作為根節點
        root = new Entry<K,V>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    //用來儲存當key不存在時,新節點