HashMap详解

什么是HashMap

        HashMap是Java中的一种数据结构,它是实现Map接口的一种典型方式,主要用于存储键值对(Key-Value pairs)

  1. 键值对存储:HashMap中的每个条目都是一个键值对,其中键(Key)是唯一的,用于标识存储的值(Value),而值可以重复。

  2. 哈希表实现:HashMap内部利用哈希表(Hash Table)来实现高效的数据存取。当向HashMap中插入一个键值对时,它会使用键的哈希码(由键的hashCode()方法生成)来确定该条目在内部数组中的存放位置。理想情况下,不同的键应该产生不同的哈希码,从而快速定位到存储位置。

  3. 冲突解决:由于不同的键可能会产生相同的哈希码(哈希碰撞),HashMap通过链地址法(链表或红黑树,取决于Java版本及桶内元素数量)来解决冲突。即在哈希表的同一个索引位置上,使用链表或树形结构链接有相同哈希值的元素。

  4. 非线程安全:默认情况下,HashMap不是线程安全的。在多线程环境下如果不采取额外的同步措施,直接并发访问HashMap可能会导致数据不一致的问题。对于需要线程安全的场景,可以使用ConcurrentHashMap

  5. 允许null键值:HashMap允许使用null作为键或值,但要注意,每个HashMap实例只能有一个null键。

  6. 性能特点:在理想情况下,HashMap的插入、删除和查找操作的平均时间复杂度均为O(1),这是因为它能够快速定位到元素的位置。但在最坏情况下(所有键的哈希码冲突),时间复杂度可能退化为O(n)。

  7. 动态扩容:当HashMap中的元素数量超过负载因子(load factor,默认为0.75)乘以当前容量时,HashMap会自动进行扩容,以保持其高效的性能。

JDK1.8中HashMap有哪些改变? 

        Java 8是一个比较大的版本,目前很多人还在用,他有很多内容的升级,关于HashMap也有很多,其中最主要的就是引入了红黑树,除此之外hash、resize等方法都有些改动。

红黑树

Java 1.7中的HashMap使用一个数组+链表的数据结构来存储键值对,这会导致在处理hash冲突时性能下降,特别是当链表变得很长的时候时。Java 1.8中的HashMap引入了红黑树来替代链表,以解决冲突时性能问题。这意味着当链表变得过长时,HashMap可以将链表转换为树,从而提高了查找、插入和删除操作的性能。

什么是Hash冲突?

        哈希冲突(Hash Collision或Hash Conflict),也称为哈希碰撞,是指在使用哈希表这一数据结构时,两个或更多的不同键(关键字)通过哈希函数计算后得到相同的哈希值,从而被映射到哈希表中的同一位置。因为哈希表的理想情况是每个键值对都能直接根据哈希值定位到一个唯一的位置上,所以哈希冲突是哈希函数的非唯一性导致的必然现象,尤其是在哈希表大小有限的情况下。

HashMap如何解决Hash冲突

        HashMap主要通过链地址法来解决哈希冲突。将Hash表的每个单位作为链表的头节点,所有hash地址为i的元素构成一个同义词链表 发生冲突就把该关键字在以该单位的节点的链表的尾部

为什么在JDK8中HashMap要引入红黑树(面试重灾区) 

        1.Java 1.7中的HashMap使用一个数组+链表的数据结构来存储键值对,这会导致在处理hash冲突时性能下降,特别是当链表变得很长的时候时。

        2.红黑树会在每次插入操作时来检查每个节点的左子树和右子树的高度差至多等于1,如果>1,就需要进行左旋或者右旋操作,使其查询复杂度一直维持在O(logN)。

        3.相对于AVL树等其他平衡树,红黑树的插入和删除操作引起结构变化的次数较少,旋转操作也更简单,这使得红黑树在实际应用中插入和删除操作的性能更好,适合需要频繁插入和删除的场景。

HashMap是如何扩容的?

        1.扩容通常发生在向HashMap中添加元素时,如果当前HashMap中的元素数量超过了阈值,这个阈值是根据当前容量和负载因子(默认为0.75)计算得到的 

        2.HashMap扩容时,会将当前容量翻倍,这样设计是因为HashMap的容量总是2的幂,这样的扩容策略可以充分利用位操作的高效性来重新计算元素在新数组中的位置。

        3.将原数组中的所有元素重新分配到新的数组中。旧数组会被垃圾回收器回收。

为什么扩容因子设置为0.75

        loadFactor是负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容。

        以下是官方文档的注释

        As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put).

        作为一般规则,默认负载因子(0.75)在时间和空间成本之间提供了一个很好的权衡。较大的值减少了空间开销,但增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。

        试想一下,如果我们把负载因子设置成1,容量使用默认初始值16,那么表示一个HashMap需要在"满了"之后才会进行扩容。

        那么在HashMap中,最好的情况是这16个元素通过hash算法之后分别落到了16个不同的桶中,否则就必然发生哈希碰撞。而且随着元素越多,哈希碰撞的概率越大,查找速度也会越低。

0.75的必然因素
理论上我们认为负载因子不能太大,不然会导致大量的哈希冲突,也不能太小,那样会浪费空间。

通过一个数学推理,测算出这个数值在0.7左右是比较合理的。

那么,为什么最终选定了0.75呢?因为threshold=loadFactor*capacity,并且capacity永远都是2的幂,为了保证负载因子(loadFactor) * 容量(capacity)的结果是一个整数,这个值是0.75(3/4)比较合理,因为这个数和任何2的幂乘积结果都是整数。

HashMap在get和put时经过哪些步骤

        

        get方法计算键的hash值,并且计算在数组中的位置,如果没有找到,表示为空,则直接返回null,如果该位置上的元素不为空,则遍历这个元素,如果与当前键值对相等的元素,则直接返回

        put方法是会先计算键的hash值(调用hash方法),通过hash值计算出索引在数组中的位置,如果这个位置上为空,则直接将键值存储到该位置上,如果不为空,则遍历该位置上的元素,是否有当前的键值对,则直接更新并且返回旧值。如果该元素不为空并且没有与之匹配的键值对,则会直接插入红黑树,如果插入成功,则直接返回替换的值,否则返回null。

还有一个问题需要注意 多线程并发扩容的时候,会产生了循环引用的问题。一直get不到值。

两个键形成环状,当下次get该桶的数据时候,如果get不到,则会一直接循环遍历,导致CPU飙升到100%

HashMap的value可以null有什么问题

        上代码

// 调用远程RPC方法,获取map
Map<String, Object> map = remoteMethod.queryMap();
// 如果包含对应key,则进行业务处理
if(map.contains(KEY)) {
    String value = (String)map.get(KEY);
    System.out.println(value);
}

虽然map.contains(key),但是map.get(key)==null,就会导致后面的业务逻辑问题

        

         

  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMap是Java中最常用的哈希表实现之一,它基于哈希表实现了Map接口。以下是HashMap源码的详细解释: HashMap内部是由一个数组和链表组成的,数组的每个元素称为桶,每个桶存储一个链表(可能为空),链表中的每个节点都是一个键值对(key-value pair)。 以下是HashMap的主要属性: ```java transient Node<K,V>[] table; // 存储元素的数组 transient int size; // 元素大小 int threshold; // 扩容阈值 final float loadFactor; // 负载因子 ``` 其中,table是一个transient修饰的Node数组,存储HashMap中的元素;size表示HashMap中元素的个数;threshold表示HashMap的扩容阈值,即当元素个数达到这个值时就需要扩容;loadFactor是负载因子,用于决定HashMap何时需要扩容。 以下是HashMap的主要方法: 1. put(K key, V value) :将指定的键值对添加到HashMap中,如果键已经存在,则更新对应的值。 2. get(Object key):获取指定键对应的值,如果键不存在则返回null。 3. remove(Object key):从HashMap中删除指定的键值对,如果键不存在则返回null。 4. clear():从HashMap中删除所有的键值对。 5. resize():扩容HashMap,将table的大小增加一倍。 6. hash(Object key):计算键的哈希值。 7. getNode(int hash, Object key):获取指定键的节点。 8. putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict):实际执行put操作的方法,会根据指定的参数决定是否更新已有键的值、是否删除过期键等。 HashMap的put方法实现如下: ```java public V put(K key, V value) { // 计算键的哈希值 int hash = hash(key); // 计算键在table数组中的索引 int i = indexFor(hash, table.length); // 遍历桶中的链表,查找指定键 for (Node<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 如果键已经存在,则更新对应的值 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 如果指定键不存在,则创建新的节点,并将其添加到桶的链表中 modCount++; addEntry(hash, key, value, i); return null; } ``` 在这个方法中,我们首先计算键的哈希值,然后计算键在table数组中的索引。接着,我们遍历桶中的链表,查找指定键,如果键已经存在,则更新对应的值。否则,我们创建新的节点,并将其添加到桶的链表中。 HashMap的get方法实现如下: ```java public V get(Object key) { // 计算键的哈希值 int hash = hash(key); // 计算键在table数组中的索引 int i = indexFor(hash, table.length); // 遍历桶中的链表,查找指定键 for (Node<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 如果找到指定键,则返回其对应的值 return e.value; } } // 如果指定键不存在,则返回null return null; } ``` 在这个方法中,我们首先计算键的哈希值,然后计算键在table数组中的索引。接着,我们遍历桶中的链表,查找指定键,如果找到指定键,则返回其对应的值。 HashMap的remove方法实现如下: ```java public V remove(Object key) { // 计算键的哈希值 int hash = hash(key); // 计算键在table数组中的索引 int i = indexFor(hash, table.length); // 遍历桶中的链表,查找指定键 Node<K,V> prev = table[i]; Node<K,V> e = prev; while (e != null) { Node<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { modCount++; size--; if (prev == e) { table[i] = next; } else { prev.next = next; } e.recordRemoval(this); return e.value; } prev = e; e = next; } // 如果指定键不存在,则返回null return null; } ``` 在这个方法中,我们首先计算键的哈希值,然后计算键在table数组中的索引。接着,我们遍历桶中的链表,查找指定键,如果找到指定键,则从链表中删除节点,并返回其对应的值。否则,我们返回null。 以上就是HashMap源码的详细解释。HashMap是一个非常常用且实用的数据结构,它的实现原理也非常值得深入学习和理解。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值