HashMap你知道多少

本文深入解析HashMap的底层结构,包括数组+链表的设计、put操作中的哈希值计算与下标确定,以及1.8前后扩容策略的转变。重点讲解了从头插法到尾插法的原因,红黑树的引入及其优势,以及线程不安全问题。

总结

首先它的底层结构是xxx,它的put方法原理是xxx。下标计算原理是xxx,当put元素达到一定值的时候会进行扩容,其扩容原理是xxx,其容量都是2的n次方,原因是xxx。
在jdk1.8之前,是头插法,1.8之后改为了尾插法,原因是xxx。
同时1.8之前和1.8之后都是线程不安全的,体现在xxx。
1.8之后还引入了红黑树,好处是xxx

1.底层结构

首先它的底层是数组+链表组成的散列表。这种能够综合数组(查找效率高)和链表(增删效率高)的优点,提高效率。

2.put

一开始数组的容量为0,HashMap会把数组长度初始化为16。当put节点的时候,会先计算这个节点的key的哈希值,然后得到节点的下标,如果此位置没有节点,则把节点放到这个下标处。如果已经有节点了,就要遍历这个节点所在的链表,看是否有key==为true或者equals方法为true的,如果有就进行value值的更新,如果没有尾插法把节点链到最后

3.key的下标/hash值是怎么计算的

具体方法是HashMap里面的hash方法,它会调用key的hashCode方法,然后将得到的哈希值,高16位和低16位进行异或运算,得到key的哈希值。然后再将此哈希值与数组长度取余(虽然是&符号,但是效果和取余效果是一样的,只是效率要比%符号高),得到下标

方法一:
static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}

哈希值的计算:将对象的hashcode()方法返回的hash值,进行无符号的右移16位,并与原来的hash值进行按位异或操作,目的是将hash的低16bit和高16bit做了一个异或,使返回的值足够散列
下标的计算:将哈希值与(数组长度-1)取余,得到下标。用&的作用是效率更高

4.扩容机制是什么?

当容量达到size*0.75的时候,会进行扩容。HashMap扩容的方法是使用一个新的数组代替原有的数组,对原数组的所有数据进行重新计算插入新数组,之后指向新数组;如果扩容前数组已经达到最大了,那么将直接将阈值设置成最大整数,然后return;

5.头插法还是尾插法,优点是啥

JDK1.8之前是头插法,1.8之后是尾插法
在并发环境下,扩容时,如果是头插法,会造成死循环。而且热点数据在扩容后会倒置,更不需要头插法了

6.红黑树

JDK1.8之后引入红黑树,数组大小大于64且链表长度大于8的时候会转成红黑树,小于64只会进行扩容。链表长度为6的时候又转回链表。红黑树是非严格平衡的二叉树,这样能够提高检索效率。

7.为啥是2的n次方

在计算buckets桶位置的时候,公式为((n-1) & hash),2的幂减去1的数的二进制数的结尾都是1,与hash值进行与运算,会得到其余数。进行按位与操作,使得结果剩下的值为对象的hash值的末尾几位,这样就我们只要保证对象的hash值生成足够散列即可
总之就是,使得((n-1) & hash)计算的索引值更均匀,能尽量减少碰撞。

8.线程不安全是影响的哪里

在jdk1.8之前,扩容(transfer方法)的时候容易形成死链和数据丢失,1.8之后,也是线程不安全的,会因为size造成数据覆盖的情况

<think> HashMapJava中最常用的集合类之一,它基于哈希表实现,用于存储键值对(key-value)映射。其底层原理主要包括以下几个方面: 1. **数组 + 链表/红黑树的结构**: - HashMap内部维护了一个`Node<K,V>[] table`数组,每个数组元素称为一个桶(bucket)。 - 当发生哈希冲突时,HashMap使用链表法解决冲突,即在同一桶中形成链表。 - 当链表长度超过阈值(默认为8)且数组长度大于等于64时,链表会转换为红黑树;当红黑树节点数小于阈值(默认为6)时,红黑树会退化为链表。 2. **哈希函数**: - HashMap通过哈希函数将键(key)映射到数组的索引位置。哈希函数的设计目标是减少冲突。 - Java 8中的哈希函数为:首先获取key的hashCode,然后通过`(h = key.hashCode()) ^ (h >>> 16)`将高16位与低16位异或,以减少碰撞。 3. **put操作流程**: - 计算key的哈希值,然后通过`(n - 1) & hash`(n为数组长度)计算数组索引。 - 如果该位置为空,则直接插入新节点。 - 如果不为空,则遍历链表或红黑树: - 如果找到相同的key(通过`equals`方法判断),则更新value。 - 如果没有找到,则在链表或红黑树中插入新节点。 - 插入后,如果链表长度超过8,则尝试将链表转换为红黑树(前提是数组长度>=64)。 - 如果数组元素总数超过阈值(容量*加载因子),则进行扩容。 4. **扩容机制**: - 默认初始容量为16,加载因子为0.75(当元素数量达到容量的75%时触发扩容)。 - 扩容时,数组大小变为原来的2倍(即左移1位),然后重新计算每个元素的位置(重新哈希)。在重新哈希时,节点在新数组中的位置要么是原位置,要么是原位置+旧数组长度。 5. **get操作流程**: - 计算key的哈希值,确定数组索引。 - 在对应的桶中遍历链表或红黑树,通过`equals`方法比较key,找到对应的节点并返回value。 6. **线程安全性**: - HashMap不是线程安全的。多线程环境下,扩容可能导致死循环(Java 8之前)或数据覆盖等问题。可以使用`ConcurrentHashMap`或`Collections.synchronizedMap`来保证线程安全。 下面是一个简单的示例代码,展示HashMap的基本使用: ```java import java.util.HashMap; import java.util.Map; public class HashMapExample { public static void main(String[] args) { // 创建HashMap Map<String, Integer> map = new HashMap<>(); // 添加键值对 map.put("Alice", 25); map.put("Bob", 30); map.put("Charlie", 28); // 获取值 System.out.println("Alice's age: " + map.get("Alice")); // 遍历HashMap for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } } } ``` §§相关问题§§: 1. HashMap和HashTable有什么区别? 2. 为什么HashMap的链表长度超过8会转为红黑树? 3. HashMap的加载因子为什么是0.75? 4. 在多线程环境下,HashMap可能会出现什么问题? </think> HashMapJava集合框架中基于哈希表的Map接口实现,用于存储键值对(key-value)映射。其底层原理如下: ### 核心原理 1. **数据结构**:数组 + 链表/红黑树 - 默认初始数组大小为16(`DEFAULT_INITIAL_CAPACITY`) - 每个数组元素称为桶(bucket),桶中存储链表或红黑树节点 - 当链表长度 > 8 且数组长度 ≥ 64 时,链表转换为红黑树 - 当红黑树节点数 < 6 时,退化为链表 2. **哈希计算** ```java // Java 8的哈希扰动函数 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` - 通过`hashCode()`的高16位异或低16位减少哈希碰撞 3. **put操作流程** 1. 计算key的哈希值 2. 通过`(n-1) & hash`确定数组下标(n为数组长度) 3. 若桶为空:直接创建新节点 4. 若桶不为空: - 链表:遍历链表,key存在则更新,否则尾插 - 红黑树:按树结构插入 5. 超过阈值(容量*加载因子)时扩容 4. **扩容机制** - 默认加载因子0.75(`DEFAULT_LOAD_FACTOR`) - 扩容时数组大小翻倍(保证始终为2的幂次) - 重新计算节点位置:新位置 = 原位置 或 原位置 + 旧容量 5. **get操作** - 计算key的哈希值定位桶 - 遍历链表/红黑树,通过`equals()`匹配key ### 关键代码实现(简化版) ```java public class HashMap<K,V> { static class Node<K,V> { final int hash; final K key; V value; Node<K,V> next; // 链表指针 } transient Node<K,V>[] table; // 存储数组 final float loadFactor = 0.75f; // 加载因子 public V put(K key, V value) { int hash = hash(key); int index = (table.length - 1) & hash; // 桶为空时直接插入 if (table[index] == null) { table[index] = new Node<>(hash, key, value, null); } else { // 遍历链表/红黑树 Node<K,V> current = table[index]; while (true) { if (current.hash == hash && (current.key == key || key.equals(current.key))) { V oldVal = current.value; current.value = value; // 更新值 return oldVal; } if (current.next == null) break; current = current.next; } current.next = new Node<>(hash, key, value, null); // 尾插 } // 扩容检查 if (size++ > threshold) resize(); return null; } void resize() { // 创建新数组(原容量*2) // 遍历旧数组所有节点,重新计算位置 // 迁移数据到新数组 } } ``` ### 线程安全问题 HashMap非线程安全,多线程环境下可能出现: 1. **死循环**(JDK7链表头插法导致) 2. **数据覆盖**(多线程同时put时) 3. **扩容丢失数据** 解决方案:使用`ConcurrentHashMap`或`Collections.synchronizedMap()` ### 设计特点 1. 允许`null`键和`null`值 2. 迭代顺序不固定(非有序集合) 3. 时间复杂度: - 理想情况:O(1) - 最坏情况(哈希冲突):O(log n)(红黑树)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值