重点是这:hashMap:https://blog.csdn.net/u012512634/article/details/72
hash表:java中哈希表及其应用详解_xiaoxik的博客-CSDN博客
hash算法:关于Java的Hash算法的深入理解_千年道士的博客-CSDN博客_hash算法
HashMap:底层是用(Entry键值对)数组+链表实现的,用链地址法来解决冲突;
HashTable:底层是用Hash表实现的,Hash表底层也是数组+链表;解决冲突方法有开放定址法,再散列函数法,链地址法;
当容量不够的时候,会进行扩容(HashMap初始长度是16):

HashMap的索引计算方法
HashMap通过put(key,value)来执行存储数据操作,通过get(key)来获取数据,而在这过程中存储的索引是非常重要的,它相当于一个坐标,有了它,就能保证我们在使用hashMap操作时的准确性。
假如调用hashMap.put(“money”,1000)方法,将会在HashMap的table数组中插入一个key是"money"的元素;这时需要通过hash()函数来确定该entry的具体插入位置,而hash()方法内部会调用hashCode()函数得到"money"的hashCode;然后putVal()方法经过一定计算得到最终的插入位置index,最终将这个entry插入到table的index位置。
key的hash值计算是通过hashCode的高16位异或低16位实现的:
h = (key.hashCode()) ^ (h>>16)
使用位运算代替了取模运算,在table的长度比较小的情况下,也能保证hashCode的高位参与到地址映射的计算当中,同时不会有太大的开销。
以下是hashmap索引过程:

拿到hash值之后,最后通过 hashmap容器长度(初始长16)length, 计算hash & (length-1)的值得到下标index;
这个方法中h代表hashcode,length代表数组长度。我们发现它是用的逻辑与操作,那么问题就来了,逻辑与操作能准确的算出来一个数组下标?我们来算算,假设hashcode是01010101(二进制表示),length为00010000(16的二进制表示),那么h & (length-1)则为:
h: 0101 0101
15: 0000 1111
&
0000 0101
对于上面这个运行结果的取值方法我们来讨论一下:因为15的高四位都是0,低四位都是1,而与操作的逻辑是两个运算位都为1结果才为1,所以对于上面这个运算结果的高四位肯定都是0,而低四位和h的低四位是一样的,所以结果的取值范围就是h的低四位的一个取值范围:0000-1111,也就是0至15,所以这个结果是符合数组下标的取值范围的。
那么假设length为17呢?那么h & (length-1)则为:
h: 0101 0101
16: 0001 0000
&
0001 0000
当length为17时,上面的运算的结果取值范围只有两个值,要么是0000 0000,要么是0001 000,这是不太好的
所以我们发现,如果我们想把HashCode转换为覆盖数组下标取值范围的下标,跟我们的length是非常相关的,length如果是16,那么减一之后就是15(0000 1111),正是这种高位都为0,低位都为1的二级制数才保证了可以对任意一个hashcode经过逻辑与操作后得到的结果是我们想要的数组下标。这就是为什么在真初始化HashMap的时候,对于数组的长度一定要是二次方数,二次方数和算数组下标是息息相关的,而这种位运算是要比取模更快的,用(length-1)保证下标index一定落在数组范围内。

装载因子α = 插入元素个数/哈希表中的数组个数,表示哈希表的装满程度,同时也能代表链表的平均长度;

如图,如果在长度为5,装载因子为0.6的哈希表中插入3个数据,接下来再插入第4个数据的时候,就会进行rehash操作;
这里的数据计算是通过计算总的数据量,即后面链表中的数据也会计算进去;
HashMap扩容是2倍 arraylist扩容1.5倍
1. 当发生地址冲突的时候(不是容量不够的时候) 链地址法:

以上是JDK1.7之前采用位桶+链表,缺点是:当冲突频繁发生时,查找时间复杂度变成了O(n);
java7及以前采用的是头插法插入节点,在扩容时会改变链表中元素的顺序,并以自己的节点作为新的头结点作为next指向,直观来看就是链表翻转了,并发线程下next指针可能被覆盖了就会出现链表成环问题;头插法链表成环问题见:
【图解】面试题:为什么HashMap会产生死循环_hashmap的死循环面试-CSDN博客
成环的流程图可以参考这个: HashMap并发时造成死循环问题解析 - 腾讯云开发者社区-腾讯云
循环形成的关键原因,多线程对链表扩容导致反转时:
- 头插法导致顺序反转:T2 迁移后链表顺序为 C → B → A;
- T1 的中间状态失效:T1 在挂起前记录的
next(即 B),其next已被 T2 修改为指向 A; - 引用混乱:T1 继续迁移时,将 A 的
next设为 B,同时 B 的next又指向 A,形成环。
JDK1.8采用位桶+链表/红黑树,当一个格子内的数据长度超过了8,就会转化为红黑树,这样查找时间变成了O(logn);
java8之后采用尾插法插入节点,扩容将保持链表元素顺序,并解决了链表成环问题
根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。
hash 冲突除了链地址法, 有那些解决办法?
开放定址法、再哈希法。
2.开放地址法
①线性探测法:如果键值对a在index中插入了,键值对b也需要插入index,那么b会去找相邻的位置(index+1,+2,+n...)进行存放;
缺点:键值对增多之后,可能会在连续的位置形成键值对,发生冲突需要很长时间才能探测到合适位置
②平方探测法:如果键值对a在index中插入了,键值对b也需要插入index,b先在index+1^2位置看是否能存放,然后再到index+2^2,+3^2..,+n^2进行存放;好处是能解决线性探测的缺点;
缺点:对于落在同一index的键值对,后面重试index一直都一样,越到后面就越需要很久探测到合适位置;
③双散列:首先可以构造一个虚拟的键值对c的散列坐标indexFake,b如果在index位置上冲突了,那么后面可能移动到的位置是index+indexFake,+2*indexFake..+n*indexFake;
3.再hash法
即重新hash一次,算出index值;
- 存储时:
- 根据键对象的
hashCode()计算哈希值,确定其在数组中的存储位置(桶位)。 - 若该位置已有元素,通过
equals()比较键是否相等:- 若相等,覆盖旧值;
- 若不等,则以链表(或红黑树)形式存储在该桶位下(解决哈希冲突)。
- 根据键对象的
- 查找时:
- 根据键对象的
hashCode()计算哈希值,定位到对应桶位。 - 遍历桶位中的元素,通过
equals()匹配目标键,找到则返回对应值。
- 根据键对象的
- 相等性一致性:
- 若
a.equals(b)为 true,则a.hashCode()必须等于b.hashCode()。 - 若
a.equals(b)为 false,a.hashCode()和b.hashCode()可以不等,但尽可能减少哈希冲突(即让不同对象的 hashCode 差异更大)。
- 若
- 对称性与传递性:
equals需满足对称性(a.equals(b)为 true 则b.equals(a)也为 true)和传递性(若a.equals(b)且b.equals(c),则a.equals(c)为 true)。
典型错误与正确实现示例
错误场景:若只重写equals而不重写hashCode,会导致:
- 存储时两个相等对象可能被分配到不同桶位,无法正确覆盖旧值;
- 查找时无法通过 hashCode 定位到目标桶位,导致查找失败。
错误场景:若只重写hashCode而不重写equals,会导致:
存储时重复键问题
场景:创建两个内容相同的 Book 对象:
-
-
Book book1 = new Book("Java编程", "张三"); Book book2 = new Book("Java编程", "张三"); Map<Book, Integer> map = new HashMap<>(); map.put(book1, 1); map.put(book2, 2); // 预期覆盖book1,实际存入两个键
-
原因:book1和book2的 hashCode 相同,但equals比较(默认比较引用)返回 false,因此 HashMap 认为是两个不同的键,导致重复存储。
查找时无法正确获取值
场景:用book2查找book1存入的值:
Integer value = map.get(book2); // 预期返回1,实际返回null
- 原因:HashMap 通过 hashCode 定位到桶位后,遍历元素时用
equals比较,由于book1.equals(book2)返回 false,无法找到目标键。
哈希冲突与性能损耗
虽然 hashCode 正确,但equals未重写会导致大量本可合并的键被视为不同键,增加链表长度,降低 HashMap 性能(退化为链表查找)
hashMap的源码:

具体如何重写equals和两种hashCode的方式:
//重写equals方法
@Override
public boolean equals(Object o) {
// 快速判断:同一对象引用直接返回true
if (this == o) return true;
// 空引用或类型不同返回false
if (o == null || getClass() != o.getClass()) return false;
// 类型转换
Student student = (Student) o;
// 比较关键属性(需覆盖所有影响对象相等性的字段)
return id == student.id && name.equals(student.name);
}
//重写hashCode的两种方法
@Override
public int hashCode() {
// 方案1:使用Objects.hash组合字段(推荐,简洁且高效)
return Objects.hash(id, name);
// 方案2:手动计算哈希(适用于性能敏感场景)
int result = 31 * id; // 31是质数,可减少冲突 (5<<i) - i 位运算方便
result += name.hashCode();
return result;
}
kotlin的data class自动编译生成:
(this.num * 31) + this.name.hashCode();
HashMap和HashTable比较:
不同:
1.HashMap支持null值和null键的,而HashTable则会抛出空指针。原因是类内部做了处理。
2.HashMap不是线程安全的,而HashTable是线程安全的。因为HashTable中的方法的是用Synchronize同步过的

如果HashMap想进行同步的话,可以用ConcurrentHashMap,采用了锁分段技术,不同的竞争资源采用不同的锁:
相较于hashtable和用了Synchronize同步的hashmap,比如一个线程再进行putall写入大量数据,期间调用线程B去get,那么线程B就会阻塞,唯一的好处就是get的时候能获取到完整更新后的全部数据
ConcurrentHashMap:一个ConcurrentHashMap包含了一个Segment数组,维护了一个HashEntry数组,每次读取数组中的数据的时候都需要获取segment对应的锁;java8之后采用cas+sychronize来代替锁分段实现;
在Java 8中,整个哈希表被分成若干个段,每个段都被实现为一个数组,每个数组元素都是一个链表或红黑树。每个元素都是一个桶,通过哈希函数将键值对映射到桶中。每个桶内部都通过synchronized来实现同步,以保证线程安全性。而对于读操作,使用了无锁的CAS操作。 相比于分段锁,这种实现方式有以下优点:
减少锁竞争:由于每个桶内部采用了synchronized来实现同步,不同的线程可以同时访问不同的桶,从而减少了锁竞争。
更高的并发度:由于不再受限于固定数量的段,ConcurrentHashMap 可以根据需要动态调整大小,并支持更高的并发度。
更好的扩展性:由于不再需要维护多个段的锁,因此在扩展时可以更容易地添加或删除桶,而不需要重构整个数据结构。
更好的性能:使用CAS操作替代了分段锁,避免了分段锁中的自旋等待开销,提高了并发性能。
3.初始化的hashMap长度是16,而hashTable初始化长度是11;
相同:HashTable除了和HashMap有上述不同,其他基本相同,比如都用哈希表来Entry对象(含键值的对象)
fail-fast:指的就是多个线程在arraylist等非线程安全的集合中进行增删等操作时,造成的异常;
HashMap中hash函数怎么是是实现的?还有哪些 hash 的实现方式?
1. 对key的hashCode做hash操作(高16bit不变,低16bit和高16bit做了一个异或);
2. h & (length-1); //通过位操作得到下标index。
还有数字分析法、平方取中法、分段叠加法、 除留余数法、 伪随机数法。?
Hash函数的其他构造方法(算hashcode):

本文介绍了HashMap和HashTable的底层实现、扩容机制、索引计算方法。当容量不足时,HashMap初始长度16,扩容2倍。JDK1.7前用位桶+链表,JDK1.8用位桶+链表/红黑树解决冲突。还提及了开放定址法、再哈希法等冲突解决办法,对比了HashMap和HashTable的差异,以及fail - fast概念。
2891

被折叠的 条评论
为什么被折叠?



