【Java源碼】集合類-JDK1.8 哈希表-紅黑樹-HashMap總結

JDK 1.8 HashMap是數組+鏈表+紅黑樹實現的,在閱讀HashMap的源碼以前先來回顧一下大學課本數據結構中的哈希表和紅黑樹。算法

什麼是哈希表?

  • 在存儲結構中,關鍵值key經過一種關係f和惟一的存儲位置相對應,關係f即哈希函數,Hash(k)=f(k)。按這個思想創建的表就是哈希表。
  • 當有兩個不相等的關鍵字key1和key2,但f(key1)=f(key2)這兩個key地址相同,就發生了衝突現象。
  • 衝突不能避免只能減小,經過設計均勻的哈希函數來減小。

經常使用哈希函數?

1. 直接定址法

Hash(key) = a*key + b (a,b爲常數)數組

取關鍵字的某種線性關係,實際中使用較少。安全

2. 初留餘數法

Hash(key) = key mod p (p,整數)數據結構

即關鍵字key除以p的餘數做爲地址。app

3.數字分析法,平方取中法,摺疊法

處理衝突的方法?

處理衝突就是爲這個關鍵字找到另外一個空的哈希地址。函數

1.開放地址法
  • 線性探測法
  • 二次探測法
  • 雙哈希函數探測法
2.拉鍊法
  • 拉鍊法的基本思想是,根據關鍵字k,將數據元素存放在哈希基表中的i=hash(k)位置上。若是產生衝突,則建立一個結點存放該數據元素,並將該結點插入到一個鏈表中。這種由衝突的數據元素構成的鏈表稱爲哈希鏈表。一個哈希基表與若干條哈希鏈表相連。
  • 例如,對於以下的關鍵字序列:{9,9,24,44,32,86,36,3,62,56}
    設哈希函數 hash(k) = k % 10,hash(k)對應哈希基表 table 的下標值 i,採用拉鍊法的哈希表結構如圖:

紅黑樹

紅黑樹本質上就是一棵二叉查找樹(二叉排序樹),紅黑樹的查找、插入、刪除的時間複雜度最壞爲O(log n)。性能

什麼是二叉查找樹(二叉排序樹)?

二叉查找樹(Binary Search Tree)也就是二叉排序樹。特徵性質:優化

  • 任意結點的左子樹不空,則左子樹上全部結點的值均小於它的根結點的值;
  • 任意結點的右子樹不空,則右子樹上全部結點的值均大於它的根結點的值;
  • 左、右子樹也爲二叉查找樹。
  • 按中序遍歷能夠獲得有序序列。

什麼是紅黑樹?

維基百科定義:https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91ui

紅黑樹(英語:Red–black tree)是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構,典型的用途是實現關聯數組。它在1972年由魯道夫·貝爾發明,被稱爲"對稱二叉B樹",它現代的名字源於Leo J. Guibas和Robert Sedgewick於1978年寫的一篇論文。紅黑樹的結構複雜,但它的操做有着良好的最壞狀況運行時間,而且在實踐中高效:它能夠在log n時間內完成查找,插入和刪除,這裏的n是樹中元素的數目。this

特徵性質:

  • 節點是紅色或黑色。
  • 根結點是黑的。
  • 全部葉子都是黑色(葉子是NIL節點)。
  • 每一個紅色節點必須有兩個黑色的子節點。(從每一個葉子到根的全部路徑上不能有兩個連續的紅色節點。)
  • 對於任一結點而言,其到葉結點的每一條路徑都包含相同數目的黑結點

JDK 1.8 Map接口

public interface Map<K,V> {
    int size(); //返回Map中鍵值對的個數
    boolean isEmpty(); //檢查map是否爲空
    boolean containsKey(Object key); //查看map是否包含某個鍵
    boolean containsValue(Object value); //查看map是否包含某個值
    V put(K key, V value); //保存,若原來有這個key則覆蓋並返回原來的值
    V get(Object key); //根據key獲取值, 若沒找到,則返回null
    V remove(Object key); //根據key刪除, 返回key原來的值,若不存在,則返回null
    void putAll(Map<? extends K, ? extends V> m); //將m中的全部鍵值對到當前的Map
    void clear(); //清空Map
    Set<K> keySet(); //返回Map中全部鍵
    Collection<V> values(); //返回Map中全部值
    Set<Map.Entry<K, V>> entrySet(); //返回Map中全部鍵值對
    //內部接口,表示一個鍵值對
    interface Entry<K,V> {
        K getKey(); //返回鍵
        V getValue(); //返回值
        V setValue(V value); //setvalue
    }
}

HashMap特色

  • 根據鍵的hashCode值存儲數據,大多數狀況下能夠直接定位到它的值,於是具備很快的訪問速度,但遍歷順序倒是不肯定的。
  • HashMap最多隻容許一條記錄的鍵爲null,容許多條記錄的值爲null。
  • HashMap非線程安全,即任一時刻能夠有多個線程同時寫HashMap,可能會致使數據的不一致。若是須要知足線程安全,能夠用Collections的synchronizedMap方法使HashMap具備線程安全的能力,或者使用ConcurrentHashMap。
  • 負載因子能夠修改,也能夠大於1,建議不要輕易修改,除非特殊狀況。

內部數據結構:

HashMap 類屬性

transient Node<k,v>[] table; 這個類屬性就是哈希桶數組

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列號
    private static final long serialVersionUID = 362498820763181265L;    
    // 默認的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30; 
    // 默認的負載因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 當桶(bucket)上的結點數大於這個值時會轉成紅黑樹
    static final int TREEIFY_THRESHOLD = 8; 
    // 當桶(bucket)上的結點數小於這個值時樹轉鏈表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中結構轉化爲紅黑樹對應的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存儲元素的數組,老是2的冪次倍(哈希桶數組)
    transient Node<k,v>[] table; 
    // 存放具體元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的個數,注意這個不等於數組的長度。
    transient int size;
    // 每次擴容和更改map結構的計數器
    transient int modCount;   
    // 臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容
    int threshold;
    // 負載因子
    final float loadFactor;
}

內部類Node

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) {
            ......
        }
        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) {
            ....
        }
        public final boolean equals(Object o) {
        ......
        }
    }

構造函數

  • 無參構造函數默認長度16,負載因子0.75
/**
     * 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
    }
  • 指定容量,負載因子0.75
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  • 指定容量和指定負載因子
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);
    }

重要函數

內部hash方法(得到的hash值用於putVal方法中肯定哈希桶數組索引位置)

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • 第一步調用object的hashCode:h = key.hashCode() 取hashCode值
  • h ^ (h >>> 16) 首先進行無符號右移(>>>)運算,再經過異或運算(^)獲得hash值。

put方法,put內部調用的是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是否是爲空,若是爲空進行擴容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            //取模運算,肯定哈希桶數組索引位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //節點key存在,直接覆蓋value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判斷是不是紅黑樹
            else if (p instanceof TreeNode)
                //若是是紅黑樹,則直接在樹中插入鍵值對
                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,大於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;
        //判斷實際存在的鍵值對數量size是否超多了最大容量threshold,若是超過,進行擴容。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  • i = (n - 1) & hash;經過取模運算,肯定哈希桶數組索引位置。位運算(&)效率要比取模運算(%)高不少,主要緣由是位運算直接對內存數據進行操做,不須要轉成十進制,所以處理速度很是快。

注意:a % b == a & (b - 1) 前提:b 爲 2^n

  • 下面是hash到肯定數組位置的過程圖:

HashMap 如何進行擴容

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 超過最大值就再也不擴充
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 沒超過最大值,擴充爲原來的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 計算新的resize上限
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
        // 把每一個bucket都移動到新的buckets中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                         // 鏈表優化重hash的代碼塊
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                             // 原索引
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 原索引+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                         // 原索引放到bucket裏
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                         // 原索引+oldCap放到bucket裏
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

注意事項

擴容是一個特別耗性能的操做,因此當使用HashMap的時候,估算map的大小,初始化的時候給一個大體的數值,避免map進行頻繁的擴容。

參考:

相關文章
相關標籤/搜索