java中的hashmap

Java hashmap原理

線性表:存儲在連續的內存地址,查詢快,插入和刪除慢。
鏈式表:存儲在間斷的,大小不固定,插入和刪除快,但是查詢的速度慢。
hashmap是以上兩種者折中的解決方案,插入或者刪除只需要動一部分即可。

HashMap的基本原理:(jdk1.7)
HashMap是基於hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。
當我們給put()方法傳遞鍵和值時,我們先對鍵調用hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry對象。

  1. 先判斷key 是否爲null
  2. 再根據key 調用hashCode()進行定位

在jdk1.8引入了紅黑樹

在這裏插入圖片描述
hashmap的主要參數

//默認初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//容量最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認加載因子0.75 (當前已佔當前最大容量的75%,會進行擴容操作)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//樹化的閾值,當桶中鏈表節點數大於8時,將鏈表轉換爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//紅黑樹退化爲鏈表的閾值,當桶中紅黑樹節點數小於6時,將紅黑樹轉換爲鏈表
static final int UNTREEIFY_THRESHOLD = 6;
//最小的樹化容量,進行樹化的時候,還有一次判斷,只有鍵值對數量大於64時纔會發生轉換,
//這是爲了避免在哈希表建立初期,多個鍵值對恰好被放入了同一個鏈表而導致不必要的轉化
static final int MIN_TREEIFY_CAPACITY = 64;

單個結點的屬性有:

hash:用於快速定位;
key:標識符
Value:存儲的數值
next:引用地址,便於插入、刪除操作

補充:
爲什麼hashmap的長度爲2的n次方?
通過hash進行定位時,hash%length 速度偏慢
若是2的n次方 hash%length==hash&(length-1) 而&操作顯然更快。

get操作:get(k)

1.判斷表是否爲空或者待查找的桶不爲空 (會先使用hashCode()計算出hash值進行哈希桶)
2.首先檢查待查找的桶的第一個元素是否是要找的元素,如果是直接返回
3.桶內紅黑樹,則調用getTreeNode()查找紅黑樹
4.桶內是鏈表,遍歷鏈表尋找節點

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //表不爲空&&表長大於0&&待查找的桶不爲空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //首先檢查桶中的第一個節點,如果相等,則直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            //如果桶中是樹結構
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //桶中是鏈表,則遍歷鏈表如果找到則直接返回
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

put操作:put(k,v)

1.如果表爲空或者表的長度爲0,調用resize初始化表,爲表分配空間
2.①二次散列處的桶爲空,直接插入元素
②桶不爲空
a)桶處的第一個節點與待插入節點的哈希相同且key“相等”,直接賦給變量e
b)桶中是紅黑樹,調用putTreeVal插入紅黑樹中
c)桶中是鏈表,遍歷鏈表,如果其中存在相同的key,則賦給變量e;不存在則尾插法加入鏈表,並判斷節點數是否大於8,如果大於8則調用treeifyBin()轉化爲紅黑樹
3.①e不爲空,替換其中的value值,並返回舊的value值
②e爲空,表大小+1,判斷是否達到了閾值,如果達到了則需要擴容

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果表爲空或者表的容量爲0,resize初始化表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根據hash得到在表中索引位置的桶,如果桶爲空,則將節點直接插入桶中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //桶不爲空
        else {
            Node<K,V> e; K k;
            //首先判斷桶中第一個節點的hash與待插入元素的key的hash值是否相同且key是否"相等",如果相等,賦給變量e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //是樹節點,則調用putTreeVal添加到紅黑樹中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //否則是鏈表,遍歷鏈表,如果不存在相同的key,則插入鏈表尾部,並且判斷節點數量是否大於樹化閾值,如果大於則轉換爲紅黑樹;如果存在相同的key,break,遍歷鏈表結束
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e不爲空表示存在相同的key,替換value並返回舊值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //鏈表元素增加,並判斷是否大於閾值,如果大於,則擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

補充說明:
在插入新節點時,JDK7採用的是頭插法,JDK8採用的是尾插法。
採用頭插法的原因是爲了熱點數據(新插入的數據)能夠優先被查找,但是這種做法有可能在多線程的情況下可能會導致死鎖( 產生死循環)。
鏈表查詢的時間複雜度是n,而紅黑樹查詢的時間複雜度是log^n

其他方法:

鏈表轉化爲紅黑樹:treeifyBin()
擴容操作:resize()

hashmap解決哈希衝突

開放定址法( 線性探測再散列,二次探測再散列,僞隨機探測再散列)
發生衝突時,形成探索序列,沿此序列逐個單元地查找,直到找到給定的哈希地址,或者碰到一個開放的地址(即該地址單元爲空)爲止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查找時探查到開放的 地址則表明表中無待查的哈希地址,即查找失敗。

鏈地址法: Java hashmap就是這麼做的
將所有哈希地址相同的結點鏈接在同一個單鏈表中。

線程安全

避免多線程對共享數據進行操作時,產生的髒讀、誤讀。
Hash線程安全:
1.多線程對相同key進行put操作
2.多線程同時對Node數組進行擴容,導致數據丟失

三種線程安全的方法:
a.hashtable 通過關鍵字synchroized標記臨界區來實現
b.ConcurrentHashMap jdk1.8引入CAS算法 它的性能最好 (CAS鎖是一種樂觀鎖,即輕量級別的鎖)
c.SyschroniedMap 通過類間接使用 syschroized
補充:syschronied的方式是一種悲觀鎖(是重量級鎖)

hashmap的常用方法:

存值: map.put(key,value)
讀值: map.get(key)
判斷是否爲空:map.isEmpty()
判斷是否含有key:map.containsKey(key)
判斷是否含有value:map.containsValue(value)
刪除key值下的value:map.remove(key) //只刪除了Value
顯示所有value值:map.values()
元素的個數:map.size()
顯示所有key:map.keySet()
顯示所有key和value:map.entrySet()
合併2個類型相同的map:map.putAll(map1)
刪除這個key和Value:map.remove(key,value)
替換key下的value:map.replace(key,newValue)
清洗整個map:map.clear()
map的克隆:map.clone()

使用hashmap時需要同時重寫 HashCode和equals方法。

單一重寫HashCode和equals都不能保證數據的唯一性。

Hashcode()的作用:將對象的內部地址轉換成一個整數返回。(即獲取hash桶的編號)
equals()的作用:只是比較2個對象之間的內容是否相同

重寫時注意:
1. equals true ->hashcode返回的int要相同
2. equals false ->hashcode 返回的int一定不相同
3. hashcode int 相同 ->equals 可以是true 也可以是false
4. hashcode int 不同 ->equals一定要是false

這裏結合 equals的知識,equals先比較 引用類型是否一致,如果一直則再比較其值是否相等。
由於HashMap不支持基本數據類型(如int),但是支持封裝類(如 Integer)
HashMap 同一個Hash桶(hashcode)裏的數據類型必須是相同的

比較 HashSet 和 HashMap

HashSet:
1.必須要重寫hasdcode 和 equals 從而確保對象的唯一性
2.利用對象獲取hashcode 元素以對象的形式
3.add()增加元素

HashMap:
1.必須要重寫hashcode 和equals
2.利用key獲取hashcode 以<key,value>的形式
3.put()增加元素

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