Java HashMap原理

HashMap是通过HashCode存储Key的,JDK1.8后引入红黑树优化查询性能。当节点数超过8时,链表转为红黑树,查询时间复杂度变为O(logn)。负载因子0.75平衡了时间和空间效率。HashMap在key的hash值右移16位进行异或操作,以减少碰撞。put操作根据key的hash值定位并覆盖或插入新节点,get操作快速根据key获取value。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、HashMap原理

HashMap就像它的名字一样,它的Key是通过HashCode来存储的,它的底层结构在JDK1.8以前是数组+链表,JDK1.8以后为了提高查询的性能加入了红黑树,首先HashMap维护了一个数组,数组中存储的元素类型为Entry,Entry就是一个键值对,当每一个Entry要被添加到数组中时,就要对这个Entry进行Hash算法来计算出它的HashCode,计算的时候是用Key来计算它的hashCode,得到它的HashCode以后,对它的HashCode进行取模运算,比如说EntryA取模的结果是1,EntryB取模的结果是2,entryC取模的结果是3,那么就根据这个值将Entry添加到对应的数组中的位置,在这个添加的过程中Hash算法是很重要的,假如一个Hash算法比较优秀的话,他计算出来的HashCode是比较分散、比较均匀的, 这样的话每一个元素才能均匀的分布在数组中,这样的话就认为Hash算法是比较优秀的,这样的元素分布可以让HashMap的一个查找的时间复杂度达到O(1),如果这个Hash算法比较差,它计算出来的HashCode取模的结果都一样的,这样的话数组就会退化成一个链表,查询的时间复杂度就变成了O(n),这样的话查找效率的差异是非常大的,所以在JDK8中对这个做了优化,就是如果一个数组中的节点数值超过8的时候,就将这个链表转换为红黑树,红黑树它的查询时间复杂度是O(logn),这样的话就起到了一定的优化作用。
节点数大于8时转换为红黑树是泊松分布,综合考虑到时间和空间的一个开销。

  • 结构中数组总存储的元素叫Node, Node是一个对Entry的实现,Entry是一个接口,node是它的一个实现类。
  static class Node<K,V> implements Map.Entry<K,V> {
          final int hash;
          final K key;
          V value;
          Node<K,V> next;//因为可能会拓展为链表,next存在
  }

-除了table是一个node数组之外的其他属性

  transient Node<K,V>[] table;
  transient Set<Map.Entry<K,V>> entrySet;//Node的上层接口,用来存放所有的Entry,用于遍历等操作
  transient int size;//是table中被使用的实际的元素数量
  transient int modCount;//计数器,记录HashMap结构发生变化的次数,比如put了一个新的值,发生了扩容等
  int threshold;//扩容的临界值threshold = size * loadFactor,
  final float loadFactor;//负载因子
  • 扩容

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // static final int MAXIMUM_CAPACITY = 1 << 30;
    

    当table中的元素被使用到了75%以上的时候,就要进行对这个hashMap 进行扩容,threshold就是这个扩容的临界值,table中的元素达到这个threshold之后,就会进行扩容,每次进行扩容的时候都是成倍的,扩容之后的大小就是之前的2倍,扩容是一个比较耗资源的操作,但是它的好处就是说,因为你的数组长度更长了,所以Hash算法的结果能够更均匀,这样的话也可以减少hash碰撞的次数。

  • 为什么负载因子是0.75: 当负载因子是1.0的时候,也就意味着,只有当数组的8个值全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。
    负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。
    但是,兄弟们,这时候空间利用率就会大大的降低,原本存储1M的数据,现在就意味着需要2M的空间。 一句话总结就是负载因子太小,虽然时间效率提升了,但是空间利用率降低了。

  • 为什么table的长度是2的次幂: 为了在取模的时候做优化吧,当你判断一个元素要进入数组的哪个桶的时候,就要对这个key的HashCode进行取模运算,但是这个取模运算是一个比较重操作吧,它比较消耗性能,但是当这个table数组的长度为2的幂次方的时候,用hashcode & (table.length - 1)的运算等价于对table.length的取模运算结果,位于运算相比于取模运算的话它的性能更好,因为位运算的操作是直接对二进制的数据进行计算的,而取模操作需要将hash值转换为十进制之后在进行运算,所以&运算再性能上有很大的优势。

    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    // 在扩容时,将旧的table中的元素添加到新的table中的时候,
    //使用了将e.hash & (newCap - 1)的这样一个操作,
    
  • 为什么将key的hash值右移16位在进行亦或的好处: 主要是为了保留高位和低位的信息,这样的画就能表现目标元素的特征,相当于减少了hash碰撞的次数。

    例子:
    A:1110 1110 & 111 的时候,它的高四位的数据就会全部丢失,因为&的111的高四位全部是0,所以得出的结果就是110,对A进行右移四位之后得到0000 1110 ,再将你这旧A的值和新的A的值进行异或(^)操作之后,得到的结果就是1110 0000,再将这个异或之后的值和table.length -1 进行与运算时就可以使计算出来的Hash更分散吧,因为异或之后的值混合了高四位和低四位的数据信息。

  • 为什么hash % table.length == hash & (table.length - 1): 对无符号数的取模和取余是一样的,假设length == 2的幂次,哈希值对2 ^ n取余,就是将hash值的二进制右移 n 位,移动的n位就是余数 , length = 2^n,length -1 的结果就是 11…1 ,n个1组成, 它和这个hash值做&操作结果和取模一样

    假设hash值是 1101 1101221% 10(2) 取模的结果1,
    将hash值和length -1 相与的结果也是1 
    
  • put操作: 当我们想往一个 HashMap 中添加一对 key-value 时,系统首先会计算 key 的 hash 值,然后根据 hash 值确认在 table 中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依次比较其 key 的 hash 值。如果两个 hash 值相等且 key 值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的 Entry 的 value 覆盖原来节点的 value。如果两个 hash 值相等但 key 值不等 ,则将该节点插入该链表的链头。

  • get操作:
    通过 key 的 hash 值找到在 table 数组中的索引处的 Entry,然后返回该 key 对应的 value 即可。在这里能够根据 key 快速的取到 value 除了和 HashMap 的数据结构密不可分外,还和 Entry 有莫大的关系。HashMap 在存储过程中并没有将 key,value 分开来存储,而是当做一个整体 key-value 来处理的,这个整体就是Entry 对象。同时 value 也只相当于 key 的附属而已。在存储的过程中,系统根据 key 的 HashCode 来决定 Entry 在 table 数组中的存储位置,在取的过程中同样根据 key 的 HashCode 取出相对应的 Entry 对象(value 就包含在里面)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值