什么是Hash
将任意长度的输入,通过hash算法,转变为固定长度的输出,该输出的值就是hash值,器存储数据的结构就是hash表。
冲突解决方法
hash表的高低16位取异或&2的N次幂是什么
对hash表中的元素执行高低16位取异或&2的N次幂操作,可以达到以下目的:
- 扰乱原始hash值,增加hash表的碰撞性:通过异或和与运算,可以有效地将hash值的高低16位混淆,减少hash冲突可能性,增加hash表的利用率。
- 加强hash表的安全性:通过扰乱原始hash值,可以隐藏hash表中元素的真实分布和映射关系,防止通过分析hash值得到的信息进行反向查找或攻击。
- 增加hash表元素的私密性:即便获取了hash表及元素的相关信息,也难以通过hash值直接获取元素的高低16位内容,能在一定程度上保护元素的内容安全。
具体的算法实现如下:
int hash_func(int key) {
int high = key >> 16; // 获得高16位
int low = key & 0xFFFF; // 获得低16位
int result = high ^ low; // 高低16位进行异或
for (int i = 0; i < n; i++) { // n为任意整数
result &= 1 << i; // 与2的i次方相与
//result &= 1 << (2 * i); // 与2的n次方相与
}
return result; // 返回结果
}
例如,如果key = 0x1234ABCD,n = 3,则:
1. high = 0x1234
2. low = 0xABCD
3. high ^ low = 0xCB91
4. result &= 1 << 0 = 0xCB91
5. result &= 1 << 2 = 0xCB10
6. result &= 1 << 4 = 0xCB00
7. 返回0xCB00
通过异或运算换位和与运算选择掩码,将key的高低16位有效地混淆,达到增加hash表碰撞性、加强安全性和保护元素私密性的目的。
hash表的put方法源码
hashtable没65行的验空。
比较:先遍历找到要存放该map的hsah值,根据hash值找到存储空间,
然后再找该存储空间下的每一个map对象进行比较
如果出现了相同的key或value原来的value(插入在hsah表中的)会被覆盖,原value返回
出现key不同value就会按照头插法插入。
public V put(K key, V value) {
//判断table是否已经初始化
if (table == EMPTY_TABLE) {
//通过阈值初始化table,解析见2.4.2
inflateTable(threshold);
}
if (key == null)
//单独处理key=null的情况,将Entry放在table[0]的位置
//相比于其他的key少了计算hash和数组下标的过程
return putForNullKey(value);
//通过key的hashcode与一系列hashcode右移后的值进行异或运算
//目的在于使hashcode的二进制值得每一位都参与到数组下标的计算中去
int hash = hash(key);
//通过hash值计算数组下标
int i = indexFor(hash, table.length);
//遍历table[i]位置的Entry链表判断是否已有相同key的Entry节点
//如果有则替换成新的value并返回旧的value
for (Entry<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;
}
}
//修改次数+1
modCount++;
//判断是否需要扩容,使用头插法插入Entry节点,源码见2.4.3
addEntry(hash, key, value, i);
return null;
}
add新元素的方法
void addEntry(int hash, K key, V value, int bucketIndex) {
// 若当前集合中节点数>=threshold(扩容的临界数量)且当前数组下标有节点,进行数组扩容操作
if ((size >= threshold) && (null != table[bucketIndex])) {
// 数组扩容为原来2倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 添加元素
createEntry(hash, key, value, bucketIndex);
}
// bucketIndex为数组的下标
void createEntry(int hash, K key, V value, int bucketIndex) {
// 保存原先bucketIndex下标的Entry对象
Entry<K,V> e = table[bucketIndex];
// 采用头插法进行插入,bucketIndex位置插入新的节点,新节点的next指针指向之前的节点
table[bucketIndex] = new Entry<>(hash, key, value, e);
// 节点数量+1
size++;
}
头插的代价最大,所以选头插
1.7多线程扩容可能会形成环状结构,不存在null结点,就会一直插入
map的扩容机制:
HashMap的扩容机制是HashMap在进行put操作时,如果发现容量已达到阈值(默认为当前容量的0.75),就会进行扩容操作。
HashMap的扩容操作主要包含以下步骤:
- 新建一个大小为当前大小2倍的数组newTable。
- 将当前Entry数组中的所有Entry重新映射到新表中。
- 如果某个Entry的hash值在新表的数组索引与旧表不同,该Entry的next引用指向null。
- 如果某个Entry的hash值在新表的数组索引与旧表相同,则保留其在新表的相对位置。
这种扩容机制是实现渐进式rehash的关键: - 新建一个更大的数组,并将数据按新的哈希函数分布在这个数组中。
- 旧数组中的数据既可以保持原有的相对位置保存到新数组,也可以根据新的哈希函数选择新位置保存。
- 新旧两个数组同时工作,查询操作需要同时查找两个数组。
- 写操作只会修改新数组。当所有的Entry都被移至新数组中,旧数组将会被丢弃。
HashMap的扩容操作有以下好处: - 避免在表的任意时间点上执行昂贵的rehash操作。扩容是渐进的,不会造成查询效率突降。
- 新数组的每个桶中的元素数量大致保持在阈值以下,因此查找、添加的时间复杂度仍为O(1)。
- 数据渐移,旧数组的数据逐步转移至新数组,实现平滑过渡,不会造成查询不能的窗口期。
- 元素散列到新数组的位置有可能发生变化,提高了Hash表防止雪崩效应(大量hash值相同)的能力。
综上,HashMap的扩容机制采用渐进rehash的策略,保证操作的时间复杂度,实现平滑过渡,增强了HashMap的健壮性。这是HashMap实现高效操作的关键所在。