HashMap底层原理

文章介绍了Java中的HashMap数据结构,包括位运算在哈希计算中的作用,如无符号右移和异或,以减少哈希冲突。HashMap的默认容量、负载因子和扩容策略也被提及。此外,还讨论了HashMap的线程不安全性以及从JDK7到JDK8的变化,如头插法与尾插法的区别。
摘要由CSDN通过智能技术生成

位运算:

    与& : 全1为1,其余为0

    或|  : 有1为1,其余为0

    异或^:不同为1,相同为0

    左移<<:二进制左移 高位丢弃,底为补0

    右移>>:二进制右移 

概念:

  • 散列表,K,V映射,允许null  
  • 继承AbstractMap,实现了map,cloneable,serizlizable接口
  • 实现是不同步的,线程不安全

   jdk1.8底层是 数组+链表+红黑树 ,1.8之前由 数组+链表 组成

 在这里插入图片描述

hash方法:将key经过hash计算后,转换成底层数组中的索引值,可以迅速定位到在hashmap中的位置。

hash冲突:当对一个元素hash,插入的时候,已经被占用。

源码分析

hashMap中常量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16   默认容量大小
static final int MAXIMUM_CAPACITY = 1 << 30;  //table最大容量2的30次方
static final float DEFAULT_LOAD_FACTOR = 0.75f;  //默认负载因子0.75
static final int TREEIFY_THRESHOLD = 8;   //链表树化阈值
static final int UNTREEIFY_THRESHOLD = 6;  //树降成链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64;  //当桶元素数量超过64个,并且链表达到阈值8才会进行树化
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;

   } 

hash计算

  • 先得到扰动key的hashCode   h=key.hashcode^(h>>>16)     
  • 再映射到数组下标index   (n-1) & hash
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

为什么无符号右移动16位进行高位异或运算?

    当数组长度短时候,只有地位参与运算

    右移shi为了让高位参与进来,更好的均匀散裂,减少碰撞,降低hash冲突概率。

    异或运算保证0和1概率相等。

hashMap扩容机制:

   当数组元素个数达到16 * 扩容因子 0.75 扩容原来的2倍,重新进行hash运算,重新放置元素

为什么是2的n次方

两个原因:
1.可以方便的将取余运算的逻辑转换为位运算,因为位运算效率高
以下是详细解释
HashMap容量取2的n次方,主要与hash寻址有关。在put(key,value)时,putVal()方法中通过i = (n - 1) & hash来计算key的桶的地址。其实,i = (n - 1) & hash是一个%取余操作。也就是说,HashMap是通过%运算来获得key的散列地址的。但是,%运算的速度并没有&的操作速度快。而&操作能代替%运算,必须满足一定的条件,也就是a%n=a&(n-1)仅当n是2的n次方的时候方能成立,一句话,可以方便的将取余运算转换为位运算
2.当length为2的N次方的时候,数据分布均匀,不浪费空间,减少冲突
以下是详细解释
这个原因:还是要从i = (n - 1) & hash这个计算公式说起,当n是2的n次方的时候,那么n的二进制形式就是010000000000000...这样的形式。n-1就是01111111111111111.。。。。这样的形式,这样的数字进行和hash进行与运算的时候,计算出来的最后意味可能是0也可能是1,空间不浪费;
如果 length 不是 2 的次幂,比如 length 为 15,则 length-1 为 14,对应的二进制为 1110,在于 hash 与操作,与操作的结果是最后一位都为 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大。

put流程:

1.根据键的hash码(调用键的hashcode方法)进行哈希运算(hash()),得到一个整数哈希值(不是数组的下标位置)
2. 判断哈希表是否为空或者长度是否为0,如果是,要对数组进行初始化(初始化为16),如果否,进入3
3. 根据1得到的哈希值计算数组索引(与运算(n - 1) & hash),得到一个和数组存储位置匹配的索引i(确定到桶的位置)
=================================================
4. 判断i号位置是否为null,如果null,就将键和值封装为一个Entry(Node)类型的对象进行插入,如果不为null,进入5
5. 判断i号桶中的节点和新插入的节点的key是否相同(使用equals进行判断),如果存在,覆盖原有的值,如果不存在,进入6
6. 判断i号位置是否为一个树结构,如果是一个树结构,在树中进行插入,插入的时候判断键和树中节点的键是否重复,如果重复,覆盖原有值,不重复,做完新节点插入,如果不是树结构,进入7
7. 为链表结构,对链表进行遍历,判断key是否存在,存在就覆盖,不存在就在链表中插入新的节点
8. 插入新节点后,如果i号位置的元素个数大于等于8且hash表的长度大于等于64,i号位置的所有元素转换为树结构,反之,新节点正常插入结束
9. size++
=====================================================
10. 判断是否要进行扩容,如果需要扩容,就执行Resize()进行扩容
11. 结束

什么是线程安全:

  • 单线程
  • 多线程无共享资源
  • 多线程下对共享资源能做到有序可控制访问

hashMap7 和8的区别:

  • 7用的头插法,8之后用的尾插法    头插法在多线程扩容的时候,容易出现环形链表死循环问题。
  • 扩容流程不同  8是扩容前插入键值,连同旧值一起转移,一起计算。 7是扩容后进行插入,旧的数据转移到新的数组之后,然后单独计算插入的位置。 8主要是为了减少红黑树和链表来回切换的频率。
  • 扩容后数据存储位置的计算方式不一样
  • 数据结构不一样

快速失败 fail-fast

      在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
      原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值