深入剖析HashSet和HashMap實現

HashSet是一個包含非重復元素的集合,如何實現的,要從底層實現代碼看起。

背景

首先非重復元素如何定義,看Set的描述:

More formally, sets contain no pair of elements e1 and e2 such that e1.equals(e2), and at most one null element.

Set不會找到兩個元素,并且兩個元素滿足e1.equals(e2)為true;并且最多只有一個null元素。

如果沒有重寫equals方法,查看Object類中equal方法的實現,==比較的其實是兩個對象在內存中的地址。

public boolean equals(Object obj) {
    return (this == obj);
}

說起equals方法,就不得不說hashCode方法了。Java中對于hashCode有個常規協定

The general contract of hashCode is:

  • Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
  • If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
  • It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
  • 程序執行期間,在同一個對象上執行多次hashCode方法,都返回相同的整數,前提是equals比較中所使用的字段沒有被修改。跨應用中的hashCode方法調用返回的整數不要求相同。
  • 如果兩個對象根據equals方法比較相同,那hashCode返回的整數也必須相同。
  • 如果兩個對象equals方法比較不相同,調用hashCode返回的整數不需要不同。但是程序員應該知道為不相等的對象生成不同的整數可以提高哈希表的性能。

HashSet的底層實現

HashSet的底層是通過HashMap實現的,將元素作為map的key以達到去重的目的,value使用的是同一個虛擬的Object實例。

private transient HashMap<E,Object> map;

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

public boolean add(E e) {
   return map.put(e, PRESENT)==null;
}

HashMap的底層實現

到最后我們要看HashMap的實現了,簡單說就是一個數組+鏈表的結合。

  • 默認初始容量16
  • 默認負荷系數0.75
  • Entry數組
  • 大小
  • 閾值:初始值等于初始容量
  • 負荷系數
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int size;
int threshold;
final float loadFactor;
Entry元素

Entry是鏈表的結果,key為Map中的key,value為Map中的value,hash為key的hash結果,next為下一個元素。

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
}
添加元素
  • 如果數組為空(即map初始化后第一次添加元素)擴充table
  • 如果key為null,則調用putForNullKey方法,null位于table的下標0處
  • 算出key的hash值
  • 通過hash值算出元素在table中的下標值
    • 如果該位置元素不為空,然后需要比較元素的hash值和上面算出的hash值是否相等,同時元素的key對象和要出入的key是否為同一對象(相同的地址 ==比較為true)或者equals方法是否為true。如果滿足條件,則更新該entry的value值;若不滿足則遍歷整個鏈表。
    • 如果為空直接添加新的entry。
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    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;
}
擴充table

對toSzie算出最小的2的冪值,用了Integer.highestOneBit((toSize -1) << 1)。減一之后左移一位,然后取最高位值,其余為補0。

為什么數組長度必須為2的冪值,請繼續看。

/**
 * 擴充table
**/
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}
計算hash值

hashSeed值為0,將key的hashCode值做多次位移和異或運算

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
計算元素位置

這里的邏輯很簡單:將hash值跟數組長度-1做了按位與。

在進行查找的時候是通過key的hash值,如果我們將元素的位置分布得盡量均勻一些,盡量做到每個位置上只有一個元素,達到O(1)的查找。這種查找通過取余就可以做到,在Java中如何做到比較快的取余呢,答案是位與運算。

上面擴充數組的時候我們保證長度為2的冪值,那減一之后就是每位都是1。做位與運算就能保證低位不同的hash值會落在不同的位置上,降低沖突(碰撞),最大程度做到均勻分布,減少鏈表的出現(查找變成O(n))。

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}
添加entry

添加新的元素時要檢查元素個數是否達到閾值,否則要做擴容處理,新table的容量為當前table長度的兩倍。

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}
resize

新table的容量為當前table長度的兩倍(table.length >= size),將舊數據中的數據遷移到新的數組中,遷移的過程中要重新計算元素在新數組中的位置。網上很多地方提到這個操作rehash,但我覺得reindex反而更恰當一些。JDK中對rehash有額外的定義,就是initHashSeedAsNeeded。當新的容量>=jdk.map.althashing.threshold的配置時,會重新計算key的hash值,即hash(e.key)。

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, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
reindex
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

原文鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容

  • 實際上,HashSet 和 HashMap 之間有很多相似之處,對于 HashSet 而言,系統采用 Hash 算...
    曹振華閱讀 2,519評論 1 37
  • 一、基本數據類型 注釋 單行注釋:// 區域注釋:/* */ 文檔注釋:/** */ 數值 對于byte類型而言...
    龍貓小爺閱讀 4,285評論 0 16
  • 1.什么是HashMap 基于哈希表的Map接口的非同步實現 此實現提供所有可選的映射操作,并允許使用null值和...
    蒼賢閱讀 513評論 0 1
  • 見天
    黃夢仙閱讀 150評論 0 0
  • 他或許會停在雨季 寒冬的風里裹著畸形的雨 帶著痛苦,也帶著恨意 想起你 他會停在雨季 那些如竹筍般放肆生長的 忘記...
    啤酒瘦肉閱讀 264評論 0 3