java基礎-------HashMap(JDK1.8)

散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表

 目錄:

1.什麼是hash表

2.HashMap實現原理

3.HashMap的特性

4.總結

一,什麼是哈希表

哈希表是一種通過關鍵碼值(key value)直接進行訪問的數據結構。因爲在數組中,通過下標可直接確定數組元素,所以哈希表利用這一特性,將關鍵字利用某個函數映射到數組的位置中。

關係可描述爲:

位置=F(關鍵字),F爲哈希函數

二:HashMap實現原理

1.內部類

(1)Node類,實現了Map.entry<K,V>接口,是hashMap得基本組成單元,本質是一個映射

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
//構造函數的hash值,鍵,值,下一節點
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = 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) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

(2)紅黑樹,內容過多,另開一篇

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 父節點
        TreeNode<K,V> left;//左子樹
        TreeNode<K,V> right;//右子樹
        TreeNode<K,V> prev;    // 兄弟節點
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

        /**
         * Returns root of tree containing this node.
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }

(3)位桶,hashMap的主幹

   transient Node<K,V>[] table;

以上就是hashMap裏的基本數據結構,hashMap主要實現方式爲數組加鏈表加紅黑樹。具體實現下面詳細給出。

2.hashMap詳解

(1)主要成員值

//初始容器大小爲16,必須爲2的次方  
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//容器最大容量
 static final int MAXIMUM_CAPACITY = 1 << 30;
//初始負載因子爲0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//鏈表轉成紅黑樹的臨界值爲8
static final int TREEIFY_THRESHOLD = 8;
//收縮檢測,紅黑樹轉成鏈表的臨界值    
static final int UNTREEIFY_THRESHOLD = 6;
//最小樹形化容量閾值,個人理解是平衡樹形化和擴容數組之間的開銷,當數組長度小於該值時,優先進行擴容
static final int MIN_TREEIFY_CAPACITY = 64;
//

(2)構造方法

I:

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

II:

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);//將map m裏的元素添加到hashMap裏
    }

tableSizeFor方法

    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;
    }//返回最接近輸入值的2的整數次冪的數

putMapEntries方法

 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
//判斷是否進行初始化
            if (table == null) { // pre-size
//取map實際長度除以負載因子+1,可以減少一次擴容
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
//若已初始化,並且數組元素個數大於閾值,進行擴容
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

構造方法有四個,差異在數組容量和負載因子上。

(3)主要方法之put方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
//調用putVal,對key進行hash運算
    }

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;
//數組爲空調用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;
//若插入位置存在值,判斷key是否相等,相等則覆蓋
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
//插入位置存在值並且key不相等,判斷是否爲樹,是的話putTreeVal
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
//遍歷節點鏈表
                for (int binCount = 0; ; ++binCount) {
//遍歷鏈表至最後節點,無相同key,在鏈表最後插入
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
//鏈表存在相同key,覆蓋
                    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;
            }
        }
//fail-fast實現
        ++modCount;
//判斷是否需要擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

tip:當使用迭代器遍歷hashMap的時候,發現這個對象的modCount和迭代器中存儲的expectedModCount不一樣時就會拋出異常,這可以說明hashMao不是線程安全的

 

(2)hashMap主要方法之treeifyBin

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
//判斷數組是否初始化或者數組長度是否小於MIN_TREEIFY_CAPACITY
//將原先單向鏈表轉換爲雙向鏈表
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
//treeify方法

//treeify方法 

  final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

 總結:

Q:爲什麼TREEIFY_THRESHOLD 和 UNTREEIFY_THRESHOLD 是8和6?

A:TREEIFY_THRESHOLD爲8的原因:根據泊松分佈概率質量函數,一個哈希桶達到 9 個元素的概率小於一千萬分之一. 選定閾值爲 8,退化閾值爲6:紅黑樹必鏈表佔用更大的內存空間且在元素較少時不一定有更好的表現。

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