HashMap:内部组成&put:、get、remove方法大致逻辑&总结

源码基于java1.8

一、传统 HashMap的缺点
(1)JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
(2)当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
(3)针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题
引用自:https://blog.csdn.net/lianhuazy167/article/details/66967698

若桶中链表元素个数大于等于8时,且map的长度大于64,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。
因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
引用自:https://blog.csdn.net/xingfei_work/article/details/79637878

HashMap主要有以下几个实例变量:

transient Node<K,V>[] table;
transient int size;
int threshold;
final float loadFactor;

PS:transient 表示易变的意思,在 Java 中,被该关键字修饰的变量不会被默认的序列化机制序列化。
table是一个Node类型的数组,称为哈希桶或哈希表,其中每个元素指向一个单向链表,链表中每一个节点表示一个键值对。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) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
}

1、hash是key的hash值,存储hash值是为了加快计算。
2、添加第一个元素时默认分配大小为16,但并不是size大于16时再进行扩展,什么时候扩展和threshold有关
3、threshold表示阈值,当size(键值对个数)大于等于threshold时考虑进行扩展。
4、loadFactor是负载因子,表示整体上table被占用的成都,是一个浮点数,默认为0.75,可以通过构造方法进行修改;
PS:一般来说threshold=table.length(默认16)*loadFactor

put方法大致逻辑

1.8的逻辑稍微复杂可参考上面的文章
该逻辑基于jdk1.7:

在这里插入图片描述
这里借鉴网上的一张图,对于1.8的逻辑描述:
在这里插入图片描述
1.8下HashMap的put方法:
PS:(length-1) & hash 能得到key在table中的位置;因为length为2的幂次方 (length-1) & hash等同于求模运算h%length.

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
  	/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key // key 的哈希值
     * @param key the key 
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value // 如果为true则不改变已经存在的值
     * @param evict if false, the table is in creation mode. // 不清楚干啥的
     * @return previous value, or null if none // 旧值,key对应的值不存在时返回null
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // tab为map的Node数组
        // n为数组的大小
        // i为key对应的Node数组下标
        // p为key对应的Node节点
        // PS:Node是一个单向链表
        if ((tab = table) == null || (n = tab.length) == 0) // 如果map为空
            n = (tab = resize()).length; // resize()方法初始化或加倍,Node数组的大小
        if ((p = tab[i = (n - 1) & hash]) == null) // (n - 1) & hash 得到key对应的Node数组下标,并赋值给i;
            tab[i] = newNode(hash, key, value, null); // 如果key对应的Node为null则创建常规(非树)节点
        else {
            Node<K,V> e; K k;
            // k为p的key
            // e表示key对应的Node对象
            // 下面这一堆if用来查找key对应的Node对象e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // p的哈希值==key的hash值且[当前Node的key==key或(key不为空且key   equals  当前节点的key)]
                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);
                        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;
                }
            }
            if (e != null) { // existing mapping for key——存在key对应的Node对象
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); // HashMap中该方法的实现为空,作为LinkedHashMap的回调,将节点移动到最后
                return oldValue;
            }
        }
        ++modCount; // 修改次数
        if (++size > threshold) // size大于等于threshold(阈值)时,调用resize进行扩容
            resize();
        afterNodeInsertion(evict); // HashMap中该方法的实现为空,作为LinkedHashMap的回调,用来判断是否需要删除最久未被访问的节点,需要的话则删除
        return null;
    }

被LinkedHashMap复写的方法:

    // Create a regular (non-tree) node
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }

    // For conversion from TreeNodes to plain nodes
    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        return new Node<>(p.hash, p.key, p.value, next);
    }

    // Create a tree bin node
    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
        return new TreeNode<>(hash, key, value, next);
    }

    // For treeifyBin
    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

get方法(jdk1.8)

在这里插入图片描述
关于get和put的方法详细介绍可以参考文首引用的文章,写的相当不错

remove方法(jdk1.8)

该部分转自:https://blog.csdn.net/weixin_42340670/article/details/81139900

/**
* 从HashMap中删除掉指定key对应的键值对,并返回被删除的键值对的值
* 如果返回空,说明key可能不存在,也可能key对应的值就是null
* 如果想确定到底key是否存在可以使用containsKey方法
*/
public V remove(Object key) {
    Node<K,V> e; // 定义一个节点变量,用来存储要被删除的节点(键值对)
    return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value; // 调用removeNode方法
}
/**
* 方法为final,不可被覆写,子类可以通过实现afterNodeRemoval方法来增加自己的处理逻辑(解析中有描述)
*
* @param hash key的hash值,该值是通过hash(key)获取到的
* @param key 要删除的键值对的key
* @param value 要删除的键值对的value,该值是否作为删除的条件取决于matchValue是否为true
* @param matchValue 如果为true,则当key对应的键值对的值equals(value)为true时才删除;否则不关心value的值
* @param movable 删除后是否移动节点,如果为false,则不移动
* @return 返回被删除的节点对象,如果没有删除任何节点则返回null
*/
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; // 声明节点数组、当前节点、数组长度、索引值
    /*
     * 如果 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p(该节点为 树的根节点 或 链表的首节点)不为空
     * 需要从该节点p向下遍历,找到那个和key匹配的节点对象
     */
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v; // 定义要返回的节点对象,声明一个临时节点变量、键变量、值变量
 
        // 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
 
        /*
         * 到这一步说明首节点没有匹配上,那么检查下是否有next节点
         * 如果没有next节点,就说明该节点所在位置上没有发生hash碰撞, 就一个节点并且还没匹配上,也就没得删了,最终也就返回null了
         * 如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在一个链表,也可能是一颗红黑树
         */
        else if ((e = p.next) != null) {
            // 如果当前节点是TreeNode类型,说明已经是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            // 如果不是树节点,那么就是一个链表,只需要从头到尾逐个节点比对即可    
            else {
                do {
                    // 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量,调出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
 
                    // 走到这里,说明e也没有匹配上
                    p = e; // 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点
                } while ((e = e.next) != null); // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点
            }
        }
 
        /*
         * 如果node不为空,说明根据key匹配到了要删除的节点
         * 如果不需要对比value值  或者  需要对比value值但是value值也相等
         * 那么就可以删除该node节点了
         */
        if (node != null && (!matchValue || (v = node.value) == value ||
                                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode) // 如果该节点是个TreeNode对象,说明此节点存在于红黑树结构中,调用removeTreeNode方法(该方法单独解析)移除该节点
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 如果该节点不是TreeNode对象,node == p 的意思是该node节点就是首节点
                tab[index] = node.next; // 由于删除的是首节点,那么直接将节点数组对应位置指向到第二个节点即可
            else // 如果node节点不是首节点,此时p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
                p.next = node.next;
            ++modCount; // HashMap的修改次数递增
            --size; // HashMap的元素个数递减
            afterNodeRemoval(node); // 调用afterNodeRemoval方法,该方法HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆写
            return node;
        }
    }
    return null;
}

PS:(length-1) & hash 能得到key在table中的位置;因为length为2的幂次方 (length-1) & hash等同于求模运算h%length.

小结:

HashMap存取时都根据键的hash值,只在对应的链表中操作,不访问别的链表,在对应的链表进行操作时也是线比较hash值,如果hash值相同再用equals进行比较。这就要求,相同对象的hashCode返回值必须相同;
1、因为内部使用链表,红黑树,和哈希值的方式实现,所以保存和取值的效率都很高,为O(1),每个单向链表往往只有一个或少数节点,根据hash值就可以快速定位;
2、HashMap只能工的键值对没有顺序,因为hash值是随机的
如果经常存取,且不要求顺序,那么HashMap是理想的选择。如果要求保持添加顺序,可以使用LinkedHashMap或TreeMap
3、 HashMap不是线程安全的,Java中还有一个Hashtable,他是java早起的容器类之一,实现了map接口,原理和HashMap类似,但没有特别的优化,它内部同事synchronized实现了线程安全。但是高并发场场景中推荐使用ConcurrentHashMap。

好了写的很多了,大家有什么指导或疑问欢迎评论沟通

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值