1.介绍
对于 HashSet 而言,其采用 Hash 算法决定元素在Set中的存储位置,这样可以保证元素的快速存取;
对于 HashMap 而言,其将 key-value 当成一个整体(Entry 对象)来处理,其也采用同样的 Hash 算法
去决定 key-value 的存储位置从而保证键值对的快速存取。虽然 HashMap 和 HashSet 实现的接口规范不同,
但是它们底层的 Hash 存储机制完全相同。实际上,HashSet 本身就是在 HashMap 的基础上实现的。
2.哈希的概念
Hash 就是把任意长度的输入(又叫做预映射),通过哈希算法,变换成固定长度的输出(通常是整型),该输出就是哈希值。
不同的输入可能会散列成相同的输出,从而不可能从散列值来唯一的确定输入值。
3.哈希表的特点
数组的特点是:寻址容易,插入和删除困难;
链表的特点是:寻址困难,插入和删除容易。
而哈希表可以综合二者的优点。事实上,哈希表有多种不同的实现方法,最经典的一种方法 —— 拉链法,我们可以将其理解为 链表的数组,如下图所示:
4.HashMap 的快速存取
在HashMap中,我们最常用的两个操作就是:put(Key,Value) 和 get(Key)。
当我们调用put方法存值时,HashMap首先会调用Key的hashCode方法,然后基于此获取Key哈希码,通过哈希码快速找到某个桶,这个位置可以被称之为 bucketIndex。理论上,hashCode 可能存在碰撞的情况,当碰撞发生时,这时会取出bucketIndex桶内已存储的元素,并通过hashCode() 和 equals() 来逐个比较以判断Key是否已存在。如果已存在,则使用新Value值替换旧Value值,并返回旧Value值;如果不存在,则存放新的键值对<Key, Value>到桶中。因此,在 HashMap中,equals() 方法只有在哈希码碰撞时才会被用到。
5.put方法分析
public V put(K key, V value) {
//当key为null时,调用putForNullKey方法,并将该键值对保存到table的第一个位置
if (key == null)
return putForNullKey(value);
//根据key的hashCode计算hash值
int hash = hash(key); // ------- (1)
//计算该键值对在数组中的存储位置(哪个桶)
int i = indexFor(hash, table.length); // ------- (2)
//在table的第i个桶上进行迭代,寻找 key 保存的位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) { // ------- (3)
Object k;
//判断该条链上是否存在hash值相同且key值相等的映射,若存在,则直接覆盖 value,并返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; // 返回旧值
}
}
modCount++; //修改次数增加1,快速失败机制
//原HashMap中无该映射,将该添加至该链的链头
addEntry(hash, key, value, i);
return null;
}
6.如何尽量保证元素均匀分布到table的每个桶中以便充分利用空间?
(1)使用hash()方法对一个对象的hashCode进行重新计算是为了防止质量低下的hashCode()函数实现。
由于hashMap的支撑数组长度总是 2 的幂次,通过右移可以使低位的数据尽量的不同,从而使hash值的分布尽量均匀。
(2)static int indexFor(int hash, int length) {
return h & (length-1);
}
当length为2的n次方时,hash&(length - 1)就相当于对length取模(hash%(length)),而且速度比直接取模要快得多,
这是HashMap在速度上的一个优化。
7.HashMap的扩容
resize()
随着HashMap中元素的数量越来越多,发生碰撞的概率将越来越大,所产生的子链长度就会越来越长,
这样势必会影响HashMap的存取速度。
为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)。但是,不得不说,
扩容是一个非常耗时的过程,因为它需要重新计算这些元素在新table数组中的位置并进行复制处理(这个过程叫做重哈希)
所以,如果我们能够提前预知HashMap 中元素的个数,那么在构造HashMap时预设元素的个数能够有效的提高HashMap的性能。
8.get方法的分析
HashMap只需通过key的hash值定位到table数组的某个特定的桶,然后查找并返回该key对应的value即可
public V get(Object key) {
// 若为null,调用getForNullKey方法返回相对应的value
if (key == null)
// 从table的第一个桶中寻找 key 为 null 的映射;若不存在,直接返回null
return getForNullKey();
// 根据该 key 的 hashCode 值计算它的 hash 码
int hash = hash(key.hashCode());
// 找出 table 数组中对应的桶
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//若搜索的key与查找的key相同,则返回相对应的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
9.HashMap与Hashtable的区别?
Hashtable的命运和Vector雷同!他们都是线程安全的,jdk1.0的产物
Hashtable不能存储null键null值;线程安全
HashMap可以存储null键null值;线程不安全
对于 HashSet 而言,其采用 Hash 算法决定元素在Set中的存储位置,这样可以保证元素的快速存取;
对于 HashMap 而言,其将 key-value 当成一个整体(Entry 对象)来处理,其也采用同样的 Hash 算法
去决定 key-value 的存储位置从而保证键值对的快速存取。虽然 HashMap 和 HashSet 实现的接口规范不同,
但是它们底层的 Hash 存储机制完全相同。实际上,HashSet 本身就是在 HashMap 的基础上实现的。
2.哈希的概念
Hash 就是把任意长度的输入(又叫做预映射),通过哈希算法,变换成固定长度的输出(通常是整型),该输出就是哈希值。
不同的输入可能会散列成相同的输出,从而不可能从散列值来唯一的确定输入值。
3.哈希表的特点
数组的特点是:寻址容易,插入和删除困难;
链表的特点是:寻址困难,插入和删除容易。
而哈希表可以综合二者的优点。事实上,哈希表有多种不同的实现方法,最经典的一种方法 —— 拉链法,我们可以将其理解为 链表的数组,如下图所示:
4.HashMap 的快速存取
在HashMap中,我们最常用的两个操作就是:put(Key,Value) 和 get(Key)。
当我们调用put方法存值时,HashMap首先会调用Key的hashCode方法,然后基于此获取Key哈希码,通过哈希码快速找到某个桶,这个位置可以被称之为 bucketIndex。理论上,hashCode 可能存在碰撞的情况,当碰撞发生时,这时会取出bucketIndex桶内已存储的元素,并通过hashCode() 和 equals() 来逐个比较以判断Key是否已存在。如果已存在,则使用新Value值替换旧Value值,并返回旧Value值;如果不存在,则存放新的键值对<Key, Value>到桶中。因此,在 HashMap中,equals() 方法只有在哈希码碰撞时才会被用到。
5.put方法分析
public V put(K key, V value) {
//当key为null时,调用putForNullKey方法,并将该键值对保存到table的第一个位置
if (key == null)
return putForNullKey(value);
//根据key的hashCode计算hash值
int hash = hash(key); // ------- (1)
//计算该键值对在数组中的存储位置(哪个桶)
int i = indexFor(hash, table.length); // ------- (2)
//在table的第i个桶上进行迭代,寻找 key 保存的位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) { // ------- (3)
Object k;
//判断该条链上是否存在hash值相同且key值相等的映射,若存在,则直接覆盖 value,并返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; // 返回旧值
}
}
modCount++; //修改次数增加1,快速失败机制
//原HashMap中无该映射,将该添加至该链的链头
addEntry(hash, key, value, i);
return null;
}
6.如何尽量保证元素均匀分布到table的每个桶中以便充分利用空间?
(1)使用hash()方法对一个对象的hashCode进行重新计算是为了防止质量低下的hashCode()函数实现。
由于hashMap的支撑数组长度总是 2 的幂次,通过右移可以使低位的数据尽量的不同,从而使hash值的分布尽量均匀。
(2)static int indexFor(int hash, int length) {
return h & (length-1);
}
当length为2的n次方时,hash&(length - 1)就相当于对length取模(hash%(length)),而且速度比直接取模要快得多,
这是HashMap在速度上的一个优化。
7.HashMap的扩容
resize()
随着HashMap中元素的数量越来越多,发生碰撞的概率将越来越大,所产生的子链长度就会越来越长,
这样势必会影响HashMap的存取速度。
为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)。但是,不得不说,
扩容是一个非常耗时的过程,因为它需要重新计算这些元素在新table数组中的位置并进行复制处理(这个过程叫做重哈希)
所以,如果我们能够提前预知HashMap 中元素的个数,那么在构造HashMap时预设元素的个数能够有效的提高HashMap的性能。
8.get方法的分析
HashMap只需通过key的hash值定位到table数组的某个特定的桶,然后查找并返回该key对应的value即可
public V get(Object key) {
// 若为null,调用getForNullKey方法返回相对应的value
if (key == null)
// 从table的第一个桶中寻找 key 为 null 的映射;若不存在,直接返回null
return getForNullKey();
// 根据该 key 的 hashCode 值计算它的 hash 码
int hash = hash(key.hashCode());
// 找出 table 数组中对应的桶
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//若搜索的key与查找的key相同,则返回相对应的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
9.HashMap与Hashtable的区别?
Hashtable的命运和Vector雷同!他们都是线程安全的,jdk1.0的产物
Hashtable不能存储null键null值;线程安全
HashMap可以存储null键null值;线程不安全
HashMap已经全面的取代了Hashtable;