Hashmap底层实现 JDK8

目录

前言

Map集合继承结构

1 底层实现

2 Put(K,V)方法分析

3 Hashmap的resize()扩容过程

4 get(key) 方法介绍

5 KeySet()、Values()、EntrySet()

6 HashIterator

6.1 属性及构造方法

6.2 hashNext()和nextNode()

6.3 reomve()

7 HashMap的remove()

7.1 removeNode()

Hashmap的线程不安全


前言

本人使用的是jdk8。

Map集合继承结构

1 底层实现

底层真正存储数据的是一个table数组,实际的数据结构是一个散列表,用链表来处理冲突,散列表相关知识可查阅数据结构教材(盗个图)。

HashMapæ°æ®ç»æå¾_thumb[13]

下面是哈希表的结构,注意这里table中并不是只是存了我们put的value,把key也存到了table,通过把它们构造成一个Node对象。这里的Node是链表的节点,可以看到它有一个next指针,指向下一个节点。

    // 存储数据的数组
    transient Node<K,V>[] table;

    // table中的Node元素
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;    // 冲突链表指针,指向下一个冲突节点
        ......
    }

2 Put(K,V)方法分析

下面貌似注释有点多,其实总结一下就是:

  1. 若table为空或长度为0,则先进行扩容,将table的容量初始化到16,扩容详见下面介绍。
  2. 通过key计算出Node在table中映射的位置,位置 = (table长度 - 1) & hash(key),而hash(key)= 
    (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)。
  3. 记映射的位置为pos,令Node p = table[pos],若p == null,则直接构造Node,插入pos处,然后跳到第7步;若不为null,则进行第4步。
  4. 比较p的key与我们插入的key是否相同,即:p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k)))为true,则相同,用我们插入的Node替换p,然后跳到第7步;为false则不相同,进行第5步。
  5. 判断p是否是红黑树,是的话调用插入红黑树节点的方法,然后跳到第7步;否则对table中pos处的冲突链表进行遍历,进行第6步。
  6. 若遍历位置节点为空,则说明遍历完冲突链表也没有冲突的key,所以直接将我们插入的数据构造成Node插入冲突链表尾部,同时判断冲突链表的节点个数,若不少于8个且hashtable的容量>=64,则将冲突单链表转化成红黑树,这样能够提升以后的查找效率,若冲突节点个数减少到6,就换回链表(是为了平衡时空复杂度);若遍历位置节点不为空,则同样比较它的key和我们插入的key是否相同,相同则替换,不相同则继续遍历下一个节点。为什么阈值是8,详见:https://blog.csdn.net/kyle_wu_/article/details/113578055
  7. 到这里就已经完成了对node的替换或插入,下一步判断改变后的table的长度是否到达了阈值,达到则扩容。if (++size > threshold)  resize();
    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;
        //若table为空或长度为0则先进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /*通过(n - 1) & hash计算出要插入数据的key在table(散列表)中的散列位置,并让P
          指向table中该位置上的原Node对象。
          如果P为null,就直接创建一个Node放到该位置就行了*/
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            /*根据key的hash值和equals()判断p与插入数据的key是否相同,都相同则e=p
              若插入数据的key非空且p.key.equals(插入数据的key),则e=p*/
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;        //最后的代码会让插入数据覆盖p
            else if (p instanceof TreeNode)    //若p是红黑树节点,则调用插入红黑树节点的方法
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {    //否则遍历p上的单链表,binCount 用来记录已遍历单链表上节点的个数
                for (int binCount = 0; ; ++binCount) {
                     //如果当前遍历节点为null,直接创建一个节点插入到该位置
                    if ((e = p.next) == null) {   
                        p.next = newNode(hash, key, value, null);
                        //若已遍历节点个数大于等于8,则把单链表转成红黑树,可以提升查找效率
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    /*当前遍历节点不为null,同时key等于插入数据的key,直接退出循环,此时e指
                      向了当前遍历的这个节点*/
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;        //以上条件都不满足,p指向e,后面e=p.next
                }
            }
            //e不为null的话,即在table中找到了和插入数据的key相同的Node,然后替换
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //oldValue 为null或onlyIfAbsent 为false时进行覆盖
                if (!onlyIfAbsent || oldValue == null)    
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //若此时table的长度到达了阈值,则进行扩容,扩容后的table长度为现在的两倍
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

3 Hashmap的resize()扩容过程

resize()方法介绍如图,每次将新table的体积扩充为原来的两倍:

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        ......
    }

这里介绍一下Hashmap的扩容过程,从上面代码可以看出当:size>threshold时,执行resize()方法。而threshold在Hashmap的构造方法中被初始设为16,后面每次resize(),都会令:threshold=newCapacity * laodFactor,故我们可以推出一个大致的扩容过程:

       hashmap的capacity到达threshold(16)——>resize()——>capacity *= 2——>threshold = capacity * loadFactor(0.75)——>.......

比如:第一次满了(是真的把长度为16的table填满了),扩容后新的容量是32,也就是说hashmap中的table数组长度为32,此时threshold = 32*0.75 = 24,也就是说下次当table中的元素数量达到24个时就开始扩容,而不是填满32个,这也就是loadFactor的作用。loadFactor本身是表示table的填满程度,是一个值在0~1之间的数,Hashmap里面就是为了实现:table装满75%的时候就进行扩容。因为table太满,哈希表产生冲突的几率就越大,但时空间利用率高;反之自推。loadFactor设置为0.75是一个对时空效率的考虑。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始最小容量16
static final int MAXIMUM_CAPACITY = 1 << 30;        //最大容量2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;    //装填因子0.75

3.1 hashmap 的长度始终为 2 的幂次方除了减少哈希冲突还有什么用?(腾讯)

提高效率。hash表的长度可以用计算元素插入的位置,插入位置 = hash(key) % length,这样能保证插入位置是在hash内部的,但是取模运算的效率不高,尤其是与&比较时,有人做过实验,在Java中,用一个变量%另一个变量1亿次,然后用这个变量&一个变量1亿次,前者的执行时间是后者的27倍,见:https://blog.csdn.net/weixin_30877181/article/details/101340719

因此,HashMap内部是这样计算插入位置的:插入位置 = hash & (length-1)。因为length是2的幂次方,所以length-1是一个连续低位全部是1的数,hash & (length-1) 就等价于 hash % length,这样就进行了等价转换,既能让key插入到hash表内部,也提升了效率。

4 get(key) 方法介绍

代码不长,看看代码中的注释应该就可以了。

    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;
        //table长度大于0且key的在table中的散列位置上的Node不为空,则执行方法
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //若first的key与参数key相同,返回first
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //否则便利冲突链表
            if ((e = first.next) != null) {
                //当前遍历节点e是红黑树节点,则按红黑树的方法来查找目标Node
                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;
    }

5 KeySet()、Values()、EntrySet()

HashMap中提供了这三个方法对table中所有元素的key、value和Node的遍历,这三个方法分别返回了KeySet、Values和EntrySet三个HashMap的内部类实例,这三个类都继承或实现了Java集合类或接口,都提供了size()、clear()、contains()、Iterator方法的实现,其中Iterator的实现不同,这里就来了解一下。

     // ########################### KeySet ######################## 
    final class KeySet extends AbstractSet<K> { 
        // 返回了KeyIterator
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        ......
    }

    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        // 只获取Node的key
        public final K next() { return nextNode().key; }
    }

 
     // ########################### Values ######################## 
    final class Values extends AbstractCollection<V> {
         // 返回了ValueIterator
        public final Iterator<V> iterator()     { return new ValueIterator(); }
        ......
    }

    final class ValueIterator extends HashIterator
        implements Iterator<V> {
        // 只获取Node的value
        public final V next() { return nextNode().value; }
    }

    // ########################### EntrySet ######################## 
    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        // 返回了EntryIterator
         public final Iterator<Map.Entry<K,V>> iterator()   {return new EntryIterator();}
        ......
    }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        // 返回整个Node
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

6 HashIterator

从上面代码可以看到,KeyIterator、ValueIterator和EntryIterator都继承了HashIterator,他们的取值和删除值的操作都来自HashIterator。

6.1 属性及构造方法

从构造方法可以看到,从索引为0处开始,直到找到table中不为空的元素。令next指向这个元素。

        Node<K,V> next;        // 指向下一个要返回的Node
        Node<K,V> current;     // 指向当前Node
        int expectedModCount;  // for fast-fail
        int index;             // 当前遍历到table中的哪个位置

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            // 从0处开始,直到找到table中不为空的元素。令next指向这个元素
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

6.2 hashNext()和nextNode()

nextNode方法直接返回next所指向的Node,同时继续寻找不为空的节点。首先从next的冲突链表上找,若冲突链表上没有则继续按构造器里面的方法找。

        public final boolean hasNext() {
            return next != null;
        }

        // 直接返回next指向的元素
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            // 这里继续为next找到下一个不为null的元素
            // 首先从next的冲突链表上找,若冲突链表上没有则按构造器里面的方法找
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

6.3 reomve()

remove方法比较简单,直接删除table中上一次通过nextNode返回的元素,不过这个方法如果调用2次,第二次current可能为null,从而抛出IllegalStateException。

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }

7 HashMap的remove()

代码入下,可以看到是调用的主要是调用的removeNode方法。

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

7.1 removeNode()

与put方法有很多相似之处,还是分两步,一是比较table[index]处,二是比较table[index]上的冲突链表,找到key相同的Node则删除。若目标元素在table[index]处,则用table[index]上的冲突链表的第一个Node取代table[index];若目标元素在冲突链表上,则直接从链表中删除

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&    // 比较table[index]
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {    // 遍历冲突链表进行比较
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)           // 说明node=p=table[index],直接让冲突链表上
                    tab[index] = node.next;   // 第一个元素替代table[index]
                else
                    p.next = node.next;      // 说明在冲突链表上匹配成功,此时node = p.next
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

Hashmap的线程不安全

见这篇文章,写的非常通俗易懂,图文并茂。http://www.importnew.com/22011.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值