JAVA容器(一)HashMap(jdk1.8)

HashMap位于java.util包下,实现Map接口。
键值对,每个键都唯一(插入重复键时,覆盖value值),只允许有一个空键。

结构:
数组+链表/红黑树,初始默认容量为16

基本元素:
size:hashmap中实际存在键值对的数量。
length:数组长度,必须为2的幂次方。
threshold:在此Load factor(负载因子,默认为0.75)和length对应下允许的最大元素数目,即与size相比较,size超过这个值就resize进行扩容。
modCount:用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。(内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化。)

构造方法:
在这里插入图片描述

索引方法:
计算hash:key的hashCode值的高16位异或低16位
计算索引:hash值对数组长度length取模(&length-1)

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }
 
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K, V>[] tab;   // 指向当前哈希数组
        Node<K, V> p;       // 指向待插入元素应当插入的位置
        ……
 // p指向hash所在的哈希槽(链)上的首个元素
        p = tab[i = (n - 1) & hash];
        ……
 }

同一哈希链/树下,key的hash值未必相同(对长度取模),key的hash值相同,hashcode也未必相同。

(图片来自美团技术团队https://tech.meituan.com/2016/06/24/java-hashmap.html)

对于(n - 1) & hash,n是数组长度,n是2的i次方,由上图可知,与n-1,索引结果就是hash值的低i位。

put方法:
图片来自美团技术团队


 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K, V>[] tab;   // 指向当前哈希数组
        Node<K, V> p;       // 指向待插入元素应当插入的位置
        int n, i;
        
        // 如果哈希数组还未初始化,或者容量无效,则需要初始化一个哈希数组
        if((tab = table) == null || (n = tab.length) == 0) {
            // 初始化哈希数组,或者对哈希数组扩容,返回新的哈希数组
            tab = resize();
            
            n = tab.length;
        }
        
        // p指向hash所在的哈希链/树上的首个元素
        p = tab[i = (n - 1) & hash];
        
        // 如果哈希槽为空,直接放置新节点存储键值对
        if(p == null) {
            tab[i] = newNode(hash, key, value, null);
            
            // 不为空,在后面链接更多的元素
        } else {
            Node<K, V> e;
            K k;
            
            /*
             * 对哈希槽中的首个元素进行判断
             *
             * 只有哈希值一致(还说明不了key是否一致),且key也相同(先判断key地址,不是同一个key再用equals()判断两个key是否值相同)时,
             * 这里才认定是存在同位元素(在HashMap中占据相同位置的元素)
             */
            if(p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
                e = p;  //e指向哈希槽首个元素,覆盖value,具体对value值的覆盖在后面(修改e的value)
                
                // 如果是红黑树结点,调用红黑树的插入方法
            } else if(p instanceof TreeNode) {
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            } else {//当前是链表
                // 遍历(binCount统计的是插入新元素之前遍历过的元素数量)
                for(int binCount = 0; ; ++binCount) {
                    // 如果没有找到同位元素,则需要插入新元素
                    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;
                    }
                    
                    /*
                     * 对哈希槽后面链接的其他元素进行判断
                     *
                     * 只有哈希值一致(还说明不了key是否一致),且key也相同(必要时需要用到equals()方法)时,
                     * 这里才认定是存在同位元素(在HashMap中占据相同位置的元素)
                     */
                    if(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        break;
                    }
                    
                    p = e;
                }
            }
            
            // 如果存在同位元素(在HashMap中占据相同位置的元素)
            if(e != null) { // existing mapping for key
                // 获取旧元素的值
                V oldValue = e.value;
                
                // 如果不需要维持原状(可以覆盖旧值),或者旧值为null
                if(!onlyIfAbsent || oldValue == null) {
                    // 更新旧值
                    e.value = value;
                }
                
                afterNodeAccess(e);
                
                // 返回覆盖前的旧值
                return oldValue;
            }
        }
        
        //不是覆盖情况,插入了新结点, HashMap的更改次数加一
        ++modCount;
        
        // 如果哈希数组的容量已超过阈值,则需要对哈希数组扩容
        if(++size>threshold) {
            // 初始化或扩容,返回新的哈希数组
            resize();
        }
        
        afterNodeInsertion(evict);
        
        // 插入新元素,返回null
        return null;
    }

扩容
如果是链表,将链表拆分,以后补充

线程不安全
jdk1.7中,多线程扩容时,由于头插法,会产生循环链表。
在这里插入图片描述
jdk1.8改成了尾插法,但在多线程put时会产生数据覆盖问题。
以下为put方法的代码,假设A、B计算出的下标相同,并且都判断了p为空,则会导致数据被覆盖。
在这里插入图片描述

关于链表长度大于8转换为红黑树:

  1. 首先链表长度大于8不一定会转成红黑树。见如下代码
final void treeifyBin(Node<K, V>[] tab, int hash) {
   int n;
   
   // 哈希数组的容量还未达到形成一棵红黑树的最低要求
   if(tab == null || (n = tab.length)<MIN_TREEIFY_CAPACITY) {
       // 初始化哈希数组,或者对哈希数组扩容,返回新的哈希数组
       resize();
    }
    else{//转换成红黑树
    ...
    }
     ....
  }
  1. 为什么不直接使用红黑树:因为树节点的大小是链表节点大小的两倍,所以只有在容器中包含足够的节点才用红黑树,来提高查询效率,也就是这时查询性能提高的优势大于内存占用较多的劣势(见源码注释)。
  2. 长度降到6会转换回链表,个人猜测6是内存和转换回链表带来的开销之间的平衡数字
  3. 为什么是8:
    在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,所以通常情况下,也就是hash 计算的结果离散好,因此链表不会过长,(需要存储很多的数据才会发生链表长度大于等于8的情况),因此不会发生从链表向红黑树的转换。
    当链表长度超过8,就可以推出hash函数不是很好了,此时不需要存储很多数据,链表也可能会过长,即超过8,此时转换为红黑树,就能提高查找的效率。

参考:
美团技术团队https://tech.meituan.com/2016/06/24/java-hashmap.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值