HashMap 源碼解析(一)之使用、構造以及計算容量

簡介

HashMap 是基於哈希表的 Map 接口的實現。 它的使用頻率是非常的高。

集合和映射

作爲集合框架中的一員,在深入之前, 讓我們先來簡單瞭解一下集合框架以及 HashMap 在集合框架中的位置。

集合框架

從圖中可以看出
1. 集合框架分爲兩種, 即集合(Collections)和映射(Map)
2. HashMap 是 AbstractMap 的子類。而 AbstractMap 實現了 Map, 因此它有 Map 的特性。
3. 通過Map接口, 可以生成集合(Collections)。

那集合(Collections)和映射(Map)是什麼關係呢?
從圖中我們可以看出, Map 和 Collection 是一種並行的關係。可以這麼理解:
1. 集合(Collectin)是一組單獨的元素, 通常應用了某種規則。 List 是按特定順序來存儲元素, 而 Set 存儲的是不重複的元素。
2. 映射(Map)是一系列 “Key-Value” 的集合。
3. 在 Map 中可以通過一定的方法產生 Collection。

HashMap 特點

很多時候, 我們都說, HashMap 具有如下的特點:
1. 根據鍵的 HashCode 存儲數據, 具有很快的訪問速度;
2. 此類不保證映射的順序,特別是它不保證該順序恆久不變;
3. 允許鍵爲 null, 但最多一條記錄;
4. 允許多條記錄的值爲 null;
5. 線程不安全。

也許你現在對這些特點的印象還不夠深刻, 在後續的源碼解析過程中, 可以一一的見識廬山真面目。

使用

HashMap 的使用應該算是很簡單的。有以下的方法時使用頻率相對來說最高的。

方法名 作用
V put(K key, V value) 將指定的值與此映射中的指定鍵關聯
V get(Object key) 返回指定鍵所映射的值;如果對於該鍵來說,此映射不包含任何映射關係,則返回 null。
int size() 返回此映射中的鍵-值映射關係數。
V remove(Object key) 從此映射中移除指定鍵的映射關係(如果存在)。
Set

public void testHashMap() {
    HashMap<String, String> animals = new HashMap<String, String>();
    animals.put("Tom", "Cat");
    animals.put("Tedi", "Dog");
    animals.put("Jerry", "Mouse");
    animals.put("Don", "Duck");

    // 遍歷方法1 鍵值視圖
    System.out.println("====================KeySet======================");
    Set<String> names = animals.keySet();
    for (String name:
         names) {
        System.out.println("KeySet: "+name+" is a " + animals.get(name));
    }

    // 通過 Entry 進行遍歷
    System.out.println("==================Entry========================");
    Set<Map.Entry<String, String>> entrys= animals.entrySet();
    for(Map.Entry<String, String> entry:entrys){
        System.out.println("Entry: "+entry.getKey()+" is a " + entry.getValue());
    }
    animals.remove("Don");
    // 通過 KeySet Iterator 進行遍歷
    System.out.println("================== KeySet Iterator after remove()========================");
    Iterator<String > iter = animals.keySet().iterator();
    while (iter.hasNext()) {
        String name = iter.next();
        String pet = animals.get(name);
        System.out.println(" KeySet Iterator : "+name+" is a " + pet);
    }
    animals.clear();
    // 通過 Entry Iterator 進行遍歷
    System.out.println("================== Entry Iterator after clear()========================");
    Iterator<Map.Entry<String, String>> entryIter = animals.entrySet().iterator();
    while (entryIter.hasNext()) {
        Map.Entry<String, String> animal = entryIter.next();
        System.out.println(" Entry Iterator : "+animal.getKey()+" is a " + animal.getValue());
    }
}

以上的例子對 HashMap 的常用的基本方法進行了使用。

構造

相關屬性

/**
 * 最大容量, 當傳入容量過大時將被這個值替換
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 *  HashMap的擴容閾值(=負載因子*table的容量),在HashMap中存儲的Node鍵值對超過這個數量時,自動擴容容量爲原來的二倍
 */
int threshold;
/**
 * 這就是經常提到的負載因子
 */
final float loadFactor;    

構造方法

HashMap 的構造方法有四個函數, 第四個暫且先不講。 前三個基本最後基本都是爲了初始化 initialCapacity 和 loadFactor 的。

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

該方法是我們最常用的, 將 loadFactor 和 其餘參數定義爲默認的值。

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

當我們需要明確指出我們的容量和負載因子時, 使用該函數。

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

當我們需要明確指出我們的容量和負載因子時, 使用該函數。

public HashMap(int initialCapacity, float loadFactor) {
    // 初始化的容量不能小於0
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 初始化容量不大於最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 負載因子不能小於 0
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

我們觀察以上的三個構造構造函數, 發現在其中並沒有對存儲的對象 table 的初始化, 源碼中也沒有代碼塊進行初始化或者其他的。其實是延遲到第一次使用時進行初始化, 在 resize() 中進行了初始化。

在構造函數中,最值得我們深究的就是 tableSizeFor 函數。在初始化時,將這個函數的返回值賦給了 threshold , 並不是說 threshold 就等於這個值了, 在後續會從新計算 threshold 的

tableSizeFor 函數

該函數是獲取大於或等於傳入容量 initialCapacity 的2的整數次冪。 試想, 如果我們自己來實現這個函數應該怎麼實現呢?

一般的算法(效率低, 不值得借鑑)

我們要計算比一個數距離最近的二次冪, 大多數人的想法,應該是一次取2的 0 次冪到 31 逐個與當前的數字進行比較, 第一個大於或等於的值就是我們想要的了。函數大致如下:

public int getNearestPowerOfTwo(int cap){
    int num=0;
    for (int i = 0; i < 31; i++) {
        if ((num = (1 << i)) >= cap){
            break;
        }
    }
    return num;
}

這是我隨手寫的, 還有很大的改進空間, 在這裏就不深究了。

tableSizeFor 函數算法

而 HashMap 中的定義如下:

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

我們先不說這個算法的原理, 來看和我之前的函數相比效率。

效率比較

public void compare(){
    long start = System.currentTimeMillis();
    for (int i = 0; i < (1 << 30); i++) {
        getNearestPowerOfTwo(i);
    }

    long end = System.currentTimeMillis();
    System.out.println((end-start));

    long start2 = System.currentTimeMillis();
    for (int i = 0; i < (1 << 30); i++) {
        tableSizeFor(i);
    }

    long end2 = System.currentTimeMillis();
    System.out.println((end2-start2));
}

結果如下:

8094

2453

也就是時間上相比是 3.3 倍左右。接下來讓我們看看其實現原理。

tableSizeFor 函數原理

核心思想

將該數的低位二進制位全部變爲1, 並加1返回。

舉個例子:

例子

低位二進制全部變爲1

int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;

其原理是:

首先, 我們忽略最高位之外的所有位數, 看圖解說:
原理

Step 1. 右移 1 位,並與之前的數做或運算。 則緊鄰的後 1 位變成了 1. 而此時已經確定了 2 個 1, 因此下一次可以右移2位。

Step 2. 右移 2 位,並與之前的數做或運算, 則緊鄰的後 2 也變成了 1. 而此時已經確定了 4 個 1, 因此下一次可以右移 4 位。

Step 3. 右移 4 位,並與之前的數做或運算, 則緊鄰的後 4 位也變成了1. 而此時已經確定了8 個 1, 因此下一次可以右移 8 位。

依次類推, 最後右移了 31 位。

1 + 2 + 4 + 8 + 16 = 31;

由於 int 類型去掉符號位之後就只剩下 31 位了,因此, 右移了 31 位之後可以保證最高位後面的數字都爲 1。

第一步爲什麼要 n = cap - 1?

如果不做該操作, 則如傳入的 cap 是 2 的整數冪, 則返回值是預想的 2 倍。

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