HashMap 算是咱们平常学习工做中遇到的比较多的一个类,它用于存储 Key-Value 键值对。HashMap 容许使用 null 键和 null 值,在计算 hash 值时,null 键的 hash 值就是 0,HashMap 并不保证在执行某些操做后键值对的顺序和原来相同,在多线程的环境下,使用 HashMap 须要注意线程安全问题。java
在 JDK1.8 以前,HashMap 底层采用数组+链表实现,即用链表处理冲突,同一 hash 值的元素都存储在一个链表里。可是当位于一个桶中的元素较多,即 hash 值相等的元素较多时,经过 key 值依次查找的效率较低。在 JDK1.8 中,HashMap 存储采用数组+链表+红黑树实现,当链表长度超过阈值 8 且数组长度超过 64 时,将链表转换为红黑树,这样大大减小了查找时间。node
在本文中,我会经过对 JDK1.7 和 JDk1.8 的比较,为你介绍以下内容:算法
增删改查方法分析数组
resize 方法分析缓存
树的实现(后续有时间再写,主要是图须要画更多)安全
问答题(必看)多线程
一些没有提到的细节,我会在最后以问答题的方式呈现。app
在 JDK1.7 中的构造方法以下:dom
// 无参构造方法
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
// 参数为容量大小
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 参数为容量大小 + 负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity; // (*)
init(); // 忽略这个
}
// 参数为一个Map的子类
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
复制代码
在 JDK1.8 中的构造方法以下:函数
// 无参构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 参数为容量大小
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 参数为容量大小 + 负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity); // (*)
}
// 参数为一个Map的子类
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
复制代码
分析:
无参构造方法的虽然写法不一样,可是实际效果是同样的,这个很容易看出来。
注意到我上面注释(*)
的地方
threshold = initialCapacity;
复制代码
this.threshold = tableSizeFor(initialCapacity);
复制代码
这两行代码都是把初始容量赋值给了threshold
变量。咱们知道,threshold
指的是 HashMap 存储元素的阈值,超过了这个阈值就会对其进行扩容操做。难道这里和咱们想的还不同?是的,这里的threshlod
只是用于暂存 HashMap 的容量,由于在 HashMap 中并不存在 capacity
这个成员变量。
所不一样的是,在 JDK1.7 中,threshold
是传入的初始容量,而在 JDK1.8 中,threshold
是传入的初始容量通过tableSizeFor
方法进行向上取最近的 2 的次幂以后的容量值。举个例子,若是传入的容量是 12,那么在 JDK1.7 中,在构造方法调用后,threshold
值为 12,在 JDK1.8 中,threshold
值为 16。
tableSizeFor
方法的源码以下:
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
复制代码
通过这一系列的位运算,若是输入值是 2 的冪,则原样返回,若是不是 2 的冪,则向上取就近的冪。至于为何能够本身列举下,这里咱们只须要知道这个方法的做用就够了。
如今我有一个问题,为何在 JDK1.7 里threshold
就不须要向上取 2 的次幂呢?答案是须要的,不过它不是在构造方法中完成的,而是在inflateTable
方法中进行了 HashMap 的初始化。
inflateTable
方法的源码以下:
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
// threshold 真正的值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
复制代码
看到上面的roundUpToPowerOf2
方法了吗?做用其实和tableSizeFor
方法是同样的,就是让容量向上取最近的 2 的次幂。
在这个方法中threshold
才是真正的进行初始化了,threshold = capacity * loadFactor
。
同时也把table
进行了初始化,我这里特别提到初始化这三个字,上面一处我也特意加粗了。我强调的缘由是在 JDK1.8 中,初始化并无相似inflateTable
这样单独的方法,而是在resize
方法中完成的,也就是说,在 JDK1.8 中,resize
等价于 JDK1.7 中的inflateTable + resize
。
咱们看传入参数为 Map 子类的构造方法。
在JDk1.7中,初始化完loadFactor
后,就直接调用inflateTable(threshold)
方法初始化 HashMap 了。最后把调用putAllForCreate
方法把全部 KV 装入新的 HashMap 中,这个方法仍是比较简单的。
putAllForCreate
方法源码:
private void putAllForCreate(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
// 点进去
putForCreate(e.getKey(), e.getValue());
}
复制代码
putForCreate
方法源码:
private void putForCreate(K key, V value) {
// 获取当前key的hash值
int hash = null == key ? 0 : hash(key);
// 找到hash值对应的bucket(哈希数组的位置)
int i = indexFor(hash, table.length);
// 若是当前bucket已经有元素占据,则继续向后找,若是找到有key相同的元素,那么覆盖原来的值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
// 当前bucket首元素没有被占据,或者当前bucket中没有相同元素,那么就在桶的第一个位置添加该元素
createEntry(hash, key, value, i);
}
复制代码
createEntry
方法源码:
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
复制代码
注意到这一行table[bucketIndex] = new Entry<>(hash, key, value, e);
说明是把新的节点放入到数组中,也就是链表的头部,JDK1.7 插入元素时头插法。
JDK1.7中的变量:
// 默认Entry数组的初始化容量,为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// Entry数组的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 初始化的Entry空数组
static final Entry<?,?>[] EMPTY_TABLE = {};
// 哈希数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// 默认的阈值,当一个键值对的键是String类型时,且map的容量达到了这个阈值,就启用备用哈希(alternative hashing)。备用哈希能够减小String类型的key计算哈希码(更容易)发生哈希碰撞的发生率。该值能够经过定义系统属性jdk.map.althashing.threshold来指定。若是该值是1,表示强制老是使用备用哈希;若是是-1则表示禁用
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
// HashMap的键值对数量
transient int size;
int threshold;
final float loadFactor;
// 结构性变化计数器
transient int modCount;
// 哈希种子值,默认为0
transient int hashSeed = 0;
复制代码
JDK1.8 中的变量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 哈希桶上的元素数量增长到此值后,将链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 哈希桶上的红黑树上的元素数量减小到此值时,将红黑树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 哈希数组的容量至少增长到此值,且知足TREEIFY_THRESHOLD的要求时,将链表转换为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;
复制代码
put
方法源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
复制代码
putVal
方法源码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; // 指向当前哈希数组
Node<K,V> p; // 指向待插入元素应当插入的位置
int n, i;
// 若是哈希数组还未初始化,或者容量无效,则须要初始化一个哈希数组
if ((tab = table) == null || (n = tab.length) == 0) {
// 初始化哈希数组,后面会将resize方法
tab = resize();
n = tab.length;
}
// p指向hash所在的哈希槽上的首个元素。 (length - 1) & hash 返回的是元素存放的索引
// 若是哈希槽为空,则在该槽上放置首个元素(普通Node)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 若是哈希槽不为空,则须要在哈希槽后面连接更多的元素
else {
Node<K,V> e;
K k;
/* * 对哈希槽中的首个元素进行判断 * * 只有哈希值一致(还说明不了key是否一致),且key也相同(必要时须要用到equals()方法)时, * 这里才认定是存在同位元素(在HashMap中占据相同位置的元素) */
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 若是该哈希槽上连接的是红黑树节点,则须要调用红黑树的插入方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 上面两种状况都是针对首个元素的判断,下面就是其余元素的判断
// 遍历哈希槽后面元素(binCount统计的是插入新元素以前遍历过的元素数量)
else {
for (int binCount = 0; ; ++binCount) {
// 若是没有找到同位元素,则须要插入新元素
if ((e = p.next) == null) {
// 插入一个普通结点
p.next = newNode(hash, key, value, null);
// 哈希槽上的元素数量增长到TREEIFY_THRESHOLD后,将从链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到相同元素,直接退出
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 走到这里就说明,存在相同元素,那么问题就是是否须要覆盖原来的元素?
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 若是onlyIfAbsent为false,或者原来的值为null,那么就覆盖
if (!onlyIfAbsent || oldValue == null)
// 更新旧值
e.value = value;
// 回调接口,不用管
afterNodeAccess(e);
return oldValue;
}
}
// HashMap的更改次数加一,只有新增和删除才会更新,修改是不会的
++modCount;
// 若是哈希数组的容量已超过阈值,则须要对哈希数组扩容
if (++size > threshold)
// 后面讲
resize();
// 回调接口,不用管
afterNodeInsertion(evict);
// 若是插入的是全新的元素,在这里返回null
return null;
}
复制代码
put
方法源码:
public V put(K key, V value) {
// 若是哈希数组还未初始化,则调用inflateTable初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 若是是key是null,那么单独调用putForNullKey添加
if (key == null)
return putForNullKey(value);
int hash = hash(key);
// 获取桶的位置
int i = indexFor(hash, table.length);
// 若是当前桶已经有元素占据,则继续向后找,若是找到有key相同的元素,那么覆盖原来的值
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;
}
}
// 标记添加操做,结构性变化
modCount++;
// 当前桶首元素没有被占据,或者当前桶中没有相同元素,那么就在桶的第一个位置添加该元素
addEntry(hash, key, value, i);
return null;
}
复制代码
addEntry
方法源码:
void addEntry(int hash, K key, V value, int bucketIndex) {
// 若是HashMap的大小超过阈值,而且当前桶不为空,那么进行扩容操做
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容到原来的两倍
resize(2 * table.length);
// 不为null进行hash
hash = (null != key) ? hash(key) : 0;
// 获取桶的位置
bucketIndex = indexFor(hash, table.length);
}
// 头插法建立新的节点
createEntry(hash, key, value, bucketIndex);
}
复制代码
resize
方法源码:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 原来大小已经达到最大值,就不扩容了
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 扩容后新的Entry数组
Entry[] newTable = new Entry[newCapacity];
// 将原来的元素转移到新的Entry数组,initHashSeedAsNeeded方法决定是否从新计算String类型的hash值
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 更新table
table = newTable;
// 更新threshold
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
复制代码
transfer
方法源码:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 获取新的桶位置
int i = indexFor(e.hash, newCapacity);
// 若是i位置原来没有值,则直接插入;有值,采用头插法
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
复制代码
initHashSeedAsNeeded
方法源码:
final boolean initHashSeedAsNeeded(int capacity) {
// 若是hashSeed != 0,表示当前正在使用备用哈希
boolean currentAltHashing = hashSeed != 0;
// 若是vm启动了且map的容量大于阈值,使用备用哈希
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
// 异或操做,若是两值同时为false,或同时为true,都算是false
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
// 改变hashSeed的值,使hashSeed!=0,rehash时String类型会使用新hash算法
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
复制代码
Holder
中维护的ALTERNATIVE_HASHING_THRESHOLD
是触发启用备用哈希的阈值,该值表示,若是 HashMap 的容量(Entry 数组大小)达到了该值,启用备用哈希。
Holder
会尝试读取 JVM 启动时传入的参数-Djdk.map.althashing.threshold
并赋值给ALTERNATIVE_HASHING_THRESHOLD
。它的值有以下含义:
在initHashSeedAsNeeded(int capacity)
方法中,会判断若是 HashMap 的容量(Entry 数组大小)是否大于等于ALTERNATIVE_HASHING_THRESHOLD
,是的话就会生成一个随机的哈希种子hashSeed
,该种子会在hash
方法中使用到。
上述操做实际上就是为了防止哈希碰撞攻击,只对 String 有效,由于 String 的hashcode
方法是公开的。咱们本身定义的类的hashcode
方法就不须要这种操做了。
在JDK1.7里,通过
resize
后的链表元素会倒置。
resize
方法源码:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 旧容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧阈值
int oldThr = threshold;
// 新容量,新阈值
int newCap, newThr = 0;
// 若是哈希数组已经初始化(非首次进来)
if (oldCap > 0) {
// 若是哈希表数组容量已经超过最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
// 将HashMap的阈值更新为容许的最大值
threshold = Integer.MAX_VALUE;
// 不须要更改哈希数组(容量未发生变化),直接返回
return oldTab;
}
// newCap = oldCap << 1 尝试将哈希表数组容量加倍,若是容量成功加倍(没有达到上限),则将阈值也加倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 若是哈希数组还未初始化(首次进来)
// 若是实例化HashMap时已经指定了初始容量,则将哈希数组当前容量初始化为与旧阈值同样大 this.threshold = tableSizeFor(initialCapacity);
else if (oldThr > 0) // initial capacity was placed in threshold
// oldThr在这里实际上就是原始capacity,由于capacity暂存在threshold里
newCap = oldThr;
// 若是实例化HashMap时没有指定初始容量,则使用默认的容量与阈值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/* * 至此,若是newThr==0,则可能有如下两种情形: * 1.哈希数组已经初始化,且哈希数组的容量还未超出最大容量, * 可是,在执行了加倍操做后,哈希数组的容量达到了上限 * 2.哈希数组还未初始化,但在实例化HashMap时指定了初始容量 */
if (newThr == 0) {
//
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft // 针对第二种状况,将阈值更新为初始容量*装载因子
: Integer.MAX_VALUE); // 针对第一种状况,将阈值更新为最大值
}
// 更新阈值
threshold = newThr;
// 至此,说明哈希数组须要初始化,或者须要扩容,即建立新的哈希数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 初始化数组,不须要扩容的话直接返回
table = newTab;
// 若是是扩容,则须要将旧元素复制到新容器
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 若是当前哈希槽上存在元素
if ((e = oldTab[j]) != null) {
// 置空该哈希槽
oldTab[j] = null;
// 若是该哈希槽上只有一个元素
if (e.next == null)
// 因为总容量变了,因此须要从新哈希
newTab[e.hash & (newCap - 1)] = e;
// 若是该哈希槽上连接了不止一个元素,且该元素是TreeNode类型
else if (e instanceof TreeNode)
// 拆分成黑树以适应新的容量要求
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 若是该哈希槽上连接了不止一个元素,且该元素是普通Node类型
else { // preserve order
// 低位链表:存放扩容以后数组下标 = 当前数组下标
Node<K,V> loHead = null, loTail = null;
// 高位链表:存放扩容以后数组下标 = 当前数组下标 + 扩容前数组大小
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 扩容前16,扩容后32,好比在最后一个哈希桶索引为15的元素进行以下操做:
// hash = 0 1111
// oldCap = 1 0000
// e.hash & oldCap = 0 0000
if ((e.hash & oldCap) == 0) {
// 若是没有尾,说明链表为空
if (loTail == null)
// 链表为空时,头节点指向该元素
loHead = e;
else
// 若是有尾,那么链表不为空,把该元素挂到链表的最后
loTail.next = e;
// 把尾节点设置为当前元素
loTail = e;
}
// hash = 1 1111
// oldCap = 1 0000
// e.hash & oldCap = 1 0000
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 低位的元素组成的链表仍是放置在原来的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位的元素组成的链表放置的位置只是在原有位置上偏移了老数组的长度个位置
// 例:hash为17在老数组放置在0下标,在新数组放置在16下标
// hash为18在老数组放置在1下标,在新数组放置在17下标
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
复制代码
分析:
threshold = newThr;
看到了吗?threshold
在这里才更新为真正的阈值,以前都是暂存容量的。
对于链表结构节点的从新分配,不一样于 JDK1.7 中须要从新进行 index 的计算,在 JDK1.8 中,是经过分组的方式存储在低位和高位链表中。
举个例子:
有一个哈希表的容量为16,其中一个元素的 hash 值为:1001 1111,那么通过计算,最后这个元素在哈希表中的位置是 15
n - 1: 0000 1111
hash:1001 1111
index:0000 1111 = 15
另有一个元素的 hash 值为:1000 1111,那么通过计算,最后这个元素在哈希表中的位置也是 15
n - 1: 0000 1111
hash:1000 1111
index:0000 1111 = 15
能够发现,在扩容前这两个元素都是存放在了索引为 15 的哈希桶中。可是扩容后就不同了,因为容量变成了原来的两倍 32,那么哈希表的索引也就会发生改变
n - 1: 0001 1111
hash:1001 1111
index:0001 1111 = 31 = 15 + 16
n - 1: 0001 1111
hash:1000 1111
index:0000 1111 = 15
注意到我加粗的数字,扩容后的索引位置貌似和 hash 值的第 5 位有关,也就是说,咱们只须要考虑第 5 位是 0 仍是 1,若是是 1 就放在高位,若是是 0 就放在低位,没错,事实就是如此,那该如何判断呢?咱们发现,哈希表原来的容量是16,转换成二进制恰好是 0001 0000,这样不就能够经过让元素的 hash 值和原来的数组容量进行 & 运算来判断第 5 位了。若是第 5 位是 1,说明存放在高位,数组索引为原位置+原数组大小,不然是 0,说明存在在低位,也就是原位置。
在 JDK1.8 中确实就是这么作的,见以下代码:
// 让元素的哈希值与扩容前的数组大小进行&运算,为0存放在低位链表loHead loTail
if ((e.hash & oldCap) == 0) {
// 若是没有尾,说明链表为空
if (loTail == null)
// 链表为空时,头节点指向该元素
loHead = e;
else
// 若是有尾,那么链表不为空,把该元素挂到链表的最后
loTail.next = e;
// 把尾节点设置为当前元素
loTail = e;
}
// 为1存放在高位链表hiHead hiTail
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
复制代码
// 低位的元素组成的链表仍是放置在 原来的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位的元素组成的链表放置的位置是在 原有位置上偏移了原来数组的长度个位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
复制代码
实际效果以下图:
remove
方法源码:
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
复制代码
removeEntryForKey
方法源码:
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
// 找到能够删除的元素,删除须要标志结构性变化
modCount++;
size--;
// 须要删除的元素恰好是桶中第一个元素,那么让table[i]指向后一个元素
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
复制代码
remove
方法源码:
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
复制代码
removeNode
方法源码:
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
Node<K,V>[] tab;
Node<K,V> p;
int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e;
K k;
V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
// p指向的是node的前一个节点
p = e;
} while ((e = e.next) != null);
}
}
/* * 从HashMap中移除匹配的元素 * 可能只须要匹配hash和key就行,也可能还要匹配value,这取决于matchValue参数 */
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 删除的是第一个节点
else if (node == p)
tab[index] = node.next;
// 删除中间的节点,node表示待删元素,即让node的前一个节点p的下一个节点指向node的下一个节点
else
p.next = node.next;
++modCount;
--size;
// 回调接口
afterNodeRemoval(node);
return node;
}
}
return null;
}
复制代码
removeNode
方法参数说明:
matchValue
:移除元素时是否须要考虑 value 的匹配问题movable
:移除元素后若是红黑树根结点发生了变化,那么是否须要改变结点在链表上的顺序get
方法源码:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
复制代码
getEntry
方法源码:
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
复制代码
根据给定的 key
和 hash
查找对应的(同位)元素,若是找不到,则返回 null
。
get
方法源码:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
复制代码
getNode
方法源码:
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n;
K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
复制代码
根据给定的 key
和 hash
查找对应的(同位)元素,若是找不到,则返回 null
。
删除和获取我都不在详细分析了,基本和添加差很少。
数组是用来肯定哈希桶的位置,利用元素的 key 的 hash 值对数组长度取模获得。链表或红黑树是用来解决 hash 冲突问题,当出现 hash 值同样的情形,就在数组上的对应位置造成一条链表或一棵树。
**PS:**这里的 hash 值并非指 hashcode,而是将 hashcode 高低十六位异或过的(JDK1.8)。
对 key 的 hashCode 进行 hash 运算,计算在哈希数组中的下标获取 bucket 位置,若是在桶的首位上就能够找到就直接返回,不然在树中找或者链表中遍历寻找。
putVal添加元素的过程:
- 若是哈希数组没有初始化,那么调用resize方法初始化哈希数组
- 获取添加元素在哈希数组中的索引,判断该位置是否有元素,若是没有,那么直接添加便可
- 若是已经有元素占用,那么判断该位置存放的是链表仍是红黑树。若是是链表,判断当前位置的第一个元素的 hashcode 和 key 是否和本身的相同,相同则由 onlyIfAbsent 肯定是否须要覆盖(或者自己是null直接覆盖);若是是红黑树,则直接调用 putTreeVal 方法存放。
- 首元素判断完后,若是不知足条件,那么开始遍历后面的节点,若是到了链表末尾仍是没有找到相同的元素,那么直接在尾部添加当前元素。若是在这期间遍历的元素数量达到树化的条件,那么须要将原来的链表转换为红黑树。
- 若是遍历期间找到和本身 hashcode 和 key 相同的元素,那么由 onlyIfAbsent 肯定是否须要覆盖(或者自己是null直接覆盖)
- 若是添加了新元素而不是覆盖原有值,须要 modCount 加1,表示发生了一次结构性变化。若是 size大于 threshold,则须要扩容resize
这个问题也就是为何 HashMap 扩容须要是2的次幂。
这里的 n 表明哈希表的长度,哈希表习惯将长度设置为 2 的 n 次方,这样刚好能够保证 (n - 1) & hash 的计算获得的索引值老是位于 table 数组的索引以内。例如:hash=15,n=16 时,结果为 15;hash=17,n=16 时,结果为 1。
但若是用 hash%n,那么若是 hash 是负数就会出现结果也是负数,而且%运算的效率低。
HashMap 在 JDK1.8 及之后的版本中引入了红黑树结构,若桶中链表元素个数大于等于 8 时,链表转换成树结构;若桶中链表元素个数小于等于 6 时,树结构还原成链表。由于红黑树的平均查找长度是 log(n),长度为 8 的时候,平均查找长度为 3,若是继续使用链表,平均查找长度为 8/2=4,这才有转换为树的必要。链表长度若是是小于等于 6,6/2=3,虽然速度也很快的,可是转化为树结构和生成树的时间并不会过短。
选择 6 和 8,中间有个差值 7 能够有效防止链表和树频繁转换(相似于复杂度震荡)。假设一下,若是设计成链表个数超过 8 则链表转换成树结构,链表个数小于 8 则树结构转换成链表,若是一个 HashMap 不停的插入、删除元素,链表个数在 8 左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
第二种回答:
由于红黑树须要进行左旋,右旋,变色这些操做来保持平衡,而单链表不须要。 当元素小于8个的时候,此时作查询操做,链表结构已经能保证查询性能。当元素大于8个的时候,此时须要红黑树来加快查询速度,可是新增节点的效率变慢了。全部才选取 8 这个数字做为链表转为红黑树的阈值,由于发生哈希冲突的几率知足泊松分布,当发生8次哈希碰撞的几率几乎为千万分之六,即之后不多会有元素再次添加到这个桶中,这样即便红黑树的新增元素效率低,也不会有多大影响了,由于几乎没有哈希桶中元素会超过8个。
固然这都得益于哈希函数设计的好,若是本身设计的哈希函数分布不均匀,好比咱们把对象的hashcode都统一返回一个常量,最终的结果就是 HashMap 会退化为一个链表,get 方法的性能降为 O(n),使用红黑树能够将性能提高到 O(log(n)),因此应该避免这种状况的发生。
用高16位与低16位进行异或
一、至于为何要这样呢?
hashcode是一个32位的值,用高16位与低16位进行异或,缘由在于求index是是用 (n-1) & hash ,若是hashmap的capcity很小的话,那么对于两个高位不一样,低位相同的hashcode,可能最终会装入同一个桶中。那么会形成hash冲突,好的散列函数,应该尽可能在计算hash时,把全部的位的信息都用上,这样才能尽量避免冲突。
二、为何使用异或运算?
经过写出真值表能够看出:异或运算为 50%的0和 50%的1,所以对于合并均匀的几率分布很是有用。
a | b | a AND b
---+---+--------
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1
a | b | a OR b
---+---+--------
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1
a | b | a XOR b
---+---+--------
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0
链地址法
开放地址法
- 线性探测。遇到哈希冲突 +1 到下一个判断
- 平方探测。遇到哈希冲突 +1 +4 +9 +16
- 二次哈希。遇到哈希冲突 + hash2(key)
再哈希法
公共溢出区域法
JDK1.7
存放新值的时候当前已有元素的个数必须大于等于阈值,且当前加入的数据发生了 hash 冲突
JDK1.8
一、初始化哈希数组时会调用 resize 方法
二、put 时若是哈希数组的容量已超过阈值,则须要对哈希数组扩容
三、在树化前,会先检查哈希数组长度,若是哈希数组的长度小于64,则进行扩容,而不是进行树化
在 JDK1.7 中,HashMap 整个扩容过程就是分别取出数组元素,通常该元素是最后一个放入链表中的元素,而后遍历以该元素为头(头插法)的单向链表元素,依据每一个被遍历元素的 hash 值计算其在新数组中的下标,而后进行交换。这样的扩容方式会将原来哈希冲突的单向链表尾部变成扩容后单向链表的头部。
而在 JDK 1.8 中,HashMap 对扩容操做作了优化。因为扩容数组的长度是 2 倍关系,因此对于假设初始 tableSize = 4 要扩容到 8 来讲就是 0100 到 1000 的变化(左移一位就是 2 倍),在扩容中只用判断原来的 hash 值和左移动的一位(newtable 的值)按位与操做是 0 或 1 就行,0 的话索引不变,1 的话索引变成原索引加上扩容前数组。
之因此能经过这种“与运算“来从新分配索引,是由于 hash 值原本就是随机的,而 hash 按位与上 newTable 获得的 0(扩容前的索引位置)和 1(扩容前索引位置加上扩容前数组长度的数值索引处)就是随机的,因此扩容的过程就能把以前哈希冲突的元素再随机分布到不一样的索引中去。
通常用 Integer、String 这种不可变类做为 HashMap 的 key。
String 最为经常使用,由于:
- 由于字符串是不可变的,因此在它建立的时候 hashcode 就被缓存了,不须要从新计算。这就使得字符串很适合做为 Map 中的键,字符串的处理速度要快过其它的键对象。这就是 HashMap中 的键每每都使用字符串。
- 由于获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是很是重要的,这些类已经很规范的覆写了 hashCode() 以及 equals() 方法。
默认 LoadFactor 值为 0.75。为何是 0.75 这个值呢?
这是由于对于使用链表法的哈希表来讲,查找一个元素的平均时间是 O(n),这里的 n 指的是遍历链表的长度,所以加载因子越大,对空间的利用就越充分,这就意味着链表的长度越长,查找效率也就越低。若是设置的加载因子过小,那么哈希表的数据将过于稀疏,对空间形成严重浪费。
Hashtable 能够看作是线程安全版的 HashMap,二者几乎“等价”(固然仍是有不少不一样)。
Hashtable 几乎在每一个方法上都加上 synchronized(同步锁),实现线程安全。
HashMap 能够经过 Collections.synchronizeMap(hashMap) 进行同步。
区别:
- HashMap 继承于 AbstractMap,而 Hashtable 继承于 Dictionary;
- 线程安全不一样。Hashtable 的几乎全部函数都是同步的,即它是线程安全的,支持多线程。而HashMap 的函数则是非同步的,它不是线程安全的。若要在多线程中使用 HashMap,须要咱们额外的进行同步处理;
- null 值。HashMap 的 key、value 均可觉得 null。Hashtable 的 key、value 都不能够为 null;
- 迭代器 (Iterator)。HashMap 的迭代器 (Iterator) 是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。因此当有其它线程改变了 HashMap 的结构(增长或者移除元素),将会抛出ConcurrentModificationException。
- 容量的初始值和增长方式都不同:HashMap 默认的容量大小是 16;增长容量时,每次将容量变为“原始容量x2”。Hashtable 默认的容量大小是 11;增长容量时,每次将容量变为“原始容量x2 + 1”;
- 添加 key-value 时的 hash 值算法不一样:HashMap 添加元素时,是使用自定义的哈希算法。Hashtable 没有自定义哈希算法,而直接采用的 key 的 hashCode()。
- 速度。因为 Hashtable 是线程安全的也是 synchronized,因此在单线程环境下它比 HashMap 要慢。若是你不须要同步,只须要单一线程,那么使用 HashMap 性能要好过 Hashtable。
由于被插入前的树结构是构建好的,一旦咱们进行添加黑色的节点,不管添加在哪里都会破坏原有路径上的黑色节点的数量平等关系,因此插入红色节点是正确的选择。