我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现(方法一+方法二):
方法一:
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); //第三步
这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、位运算。
第一步:显而易见,直接取就是了。
第二步:在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
第三步:不是 直接的位运算,由于length为2的幂次,所以可以使用“与”运算 h & (length-1)代替位运算。这样速度更快。
jdk1.7
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
// 这里是扰动函数
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
jdk 1.8
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看出扰动函数就是将key的哈希值h与右移16位后的h进行异或运算。
用一句话来概括扰动函数的作用就是:将h的hashCode右移16位并与自身相异或 相当于 使自己的高16位和低16位 相异或,得到的值既包含了自己高位的特性又包含了自己低位的特性,从而增加了之后得到的下标的不确定性,降低了碰撞的概率。
数组的第0个位置 固定放置key为null的value
如果table[i]为空,创建新节点存入
如果table[i]不为空,根据HashCode和key值在链表/红黑树中寻找目标位置并更新其value值
a. 如果没有发生碰撞
b. 如果发生碰撞,通过比较key的地址或者key的值(equals)遍历链表/红黑树获取旧值,覆盖后返回旧值
c. 如果HashMap容量达到阈值initialCapacity*loadFactor,则进行扩容
jdk1.8
新增链表转红黑树的阈值,因此在插入的时候必须知道链表的长度,如果长度超出这个阈值就将其转化为红黑树,因此在插入式必须遍历链表得到链表长度,于是在jdk1.8里插入结点时选择直接插在链表尾部,反正都要遍历一次,这样还保证了在扩容的时候对元素进行transfer时链表的顺序不会像1.7一样倒转,也就不会出现死循环链表。
jdk1.7 (size >= threshold) && (null != table[bucketIndex])
存放的键值对数超出阈值,并且新增结点要插入的地方不为空
jdk1.8 ++size > threshold
只要存放键值对数超出阈值就扩容
默认扩容16,原数组长度,构建一个原数组长度两倍的新数组,并调用transfer将原数组的数组通过重新计算哈希值得到下标再转移到新增数组。
jdk1.7调用indexFor方法重新计算下标,并采用跟插入结点时一致的方式(头插法)挨个移动结点
jdk1.8则是根据规律将原链表拆分为两组,分别记录两个头结点,移动时直接移动头结点
我们会发现 HashMap扩容后,原来的元素要么在原位置,要么在原位置+原数组长度 那个位置上
举个栗子来说:
原来的HashMap长度为4,table[2]上存放了A。现在要进行扩容,先创建了一个长度为8的新数组,现在要进行transfer,那么这个A要放到哪里呢?
我们先来根据他原本所在的位置2来倒推,我们知道index = HashCode(Key) & (Length - 1),那么就有
Hash(key) 可能为010,可能为110。
我们用新的长度(8 = (111)2)和这两个数分别再去通过Hash算法来计算新的下标会发现
010 & 111 = 010 在原位置
110 & 111 = 110 在原位置+4,当前下标+旧数组长度
在转移链表时,结点的转移和插入是一致的,jdk1.7将采用头插法(转移完后链表反转),jdk1.8在分解完链表后直接移动头结点
主要体现
jdk1.7中,当多线程操作同一map时,在扩容的时候会因链表反转发生循环链表或丢失数据的情况
jdk1.8中,当多线程操作同一map时,会发生数据覆盖的情况
在put的时候,由于put的动作不是原子性的,线程A在计算好链表位置后,挂起,线程B正常执行put操作,之后线程A恢复,会直接替换掉线程b put的值 所以依然不是线程安全的
首先我们要知道,HashMap中有个属性modCount,用于记录当前map的修改次数,在对map进行put、remove、clear等操作时都会增加modCount。
他的作用体现在对map进行遍历的时候,我们知道HashMap不是线程安全的,当对其进行遍历的时候,会先把modCount赋给迭代器内部的expectedModCount属性。当我们对map进行迭代时,他会时时刻刻比较expectedModCount和modCount是否相等,如果不相等,则说明有其他的线程对同一map进行了修改操作,于是迭代器抛出ConcurrentModificationException异常。
这就是Fail-Fast 机制。
在系统设计中,快速失效系统一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。这种设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障。快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。
构造函数
/** 参数为空的构造方法 */
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/** 自定义初始化容量的构造方法 */
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 构造一个空的HashMap
*
* @param initialCapacity 初始容量
* @param loadFactor 负载因子
* @throws IllegalArgumentException 初始容量/负载因子非法则抛出异常
*/
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); // 初始负载因子小于等于0或NaN,抛异常
this.loadFactor = loadFactor; // 将负载因子赋值给全局变量
threshold = initialCapacity; // 将初始化容量赋值给阈值
init();
}
关于负载因子:负载因子表示一个散列空间的使用程度,负载因子越大则散列表的装填程度越高,填入表中的元素越多,越容易发生冲突;负载因子越小则散列表的装填程度越低,填入表中的元素越少,越不易发生冲突,但是容易造成空间浪费。
关于最后的init(),在源码里只是个空方法,似乎是为了子类的反序列化而准备的,在hashmap构造之后、数组创建之前调用,jdk1.8以后就没有了
public V put(K key, V value) {
// 如果数组为空,初始化一个数组,长度为大于阈值的最小的2的幂次数
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 键为空,调用putForNullKey方法,因为空键固定放在0号位
if (key == null)
return putForNullKey(value);
int hash = hash(key); // 计算哈希值
int i = indexFor(hash, table.length); // 计算下标i
// 键不为空
// 遍历i号位的链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 找到键为key的结点,判断条件:
if (e.hash == hash // 1.key的哈希值相同
&& ((k = e.key) == key || key.equals(k))) { // 2.key的值相同
V oldValue = e.value;
e.value = value; // 覆写
e.recordAccess(this);
return oldValue;
}
}
// 不存在,插入
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 键值对数量超过阈值 并且 当前元素要存放的位置不为空
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); // 扩容
hash = (null != key) ? hash(key) : 0; // 重新计算哈希值
bucketIndex = indexFor(hash, table.length); // 重新计算下标
}
// 加入新结点
createEntry(hash, key, value, bucketIndex);
}
public V get(Object key) {
// key为空,调用getForNullKey方法,因为空键固定放在0号位
if (key == null)
return getForNullKey();
// 得到key所在的结点
Entry<K,V> entry = getEntry(key);
// 结点为空直接返回null,反之获取对应val
return null == entry ? null : entry.getValue();
}
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
final Entry<K,V> getEntry(Object key) {
// 空map返回null
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key); // 得到key的哈希值
// 计算key对应下标i,并遍历i号位的链表,找到key值对应的结点
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 找到键为key的结点,判断条件:
if (e.hash == hash && // 1.key的哈希值相同
((k = e.key) == key || (key != null && key.equals(k)))) // 2.key的值相同
return e;
}
// 找不到就返回null
return null;
}
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
* 把当前map中的元素翻新到新的更大的新map中。当前map中的键值对达到阈值就会触发扩容方法。
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
* 如果当前容量已经达到最大容量,map将不会进行扩容,而是将阈值提到Integer.MAX_VALUE,从而达到以后不会再调用扩容方法的效果
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
* 新的容量,必须是2的幂次,必须比当前容量大,除非当前容量已经达到最大容量
*/
void resize(int newCapacity) {
Entry[] oldTable = table; // 当前数组
int oldCapacity = oldTable.length; // 当前容量
// 已经达到最大容量的情况下,将阈值升到Integer.MAX_VALUE,能够有效阻止日后调用扩容
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity]; // 创建新数组
transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 将旧数组中的元素全部转移到新数组
table = newTable; // 新数组
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); // 重新计算阈值
}
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; // 指针记录e.next
// 如果需要,重新计算哈希值
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity); // 重新计算下标
// 头插法
e.next = newTable[i]; // 将e插入到table[i]头部之前
newTable[i] = e; // 将链表下移
e = next; // 指向下一个要移动的结点
}
}
}
static final int TREEIFY_THRESHOLD = 8; // 链表树化阈值
static final int UNTREEIFY_THRESHOLD = 6; // 树链表化阈值
Node
和1.7几乎相同,不做注释,两点不同的是
/**
* Constructs an empty HashMap with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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);
}
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
* 以给定的键值对寻找是否有相同的键值,如果有,覆写。
*
* @param key key with which the specified value is to be associated 键
* @param value value to be associated with the specified key 值
*
* @return the previous value associated with key, or
* null if there was no mapping for key.
* (A null return can also indicate that the map
* previously associated null with key.)
* 有相同的key,则返回旧值;没有就返回null
* 返回值同时可以作map中是否包含该key值的判断
*/
public V put(K key, V value) {
// 计算哈希值开始找
// onlyIfAbsent: false <=> 允许覆写
// evict: true <=> 插入结点后是否允许操作(看了下应该是给子类LinkedHashMap用的
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 数组为空 或 数组长度为0,构造table数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算下标i,数组i号位的结点p为空,直接插入新结点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// i号位不为空,遍历
Node<K,V> e; K k;
// 头结点是要找的结点(key的哈希值或key的值相同),e指向p
if (p.hash == hash && // key的哈希值相同
((k = p.key) == key || (key != null && key.equals(k)))) // 或 key值相同
e = p;
// p是树节点,调用红黑树的插入方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// p是链表结点
else {
// 遍历链表,同时计数binCount,用于判断是否该树化链表
for (int binCount = 0; ; ++binCount) {
// 遍历到尾结点
if ((e = p.next) == null) {
// 插入结点
p.next = newNode(hash, key, value, null);
// 判断是否超出阈值,是则树化
// -1是因为除去了数组内的头结点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到目标结点,赋值给e
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 找到目标结点,覆写(如果允许的话 或 旧值为null)并返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 操作次数++
// 键值对数量增加并判断是否该扩容(与1.7相比判断条件少了
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
注意两点
public V get(Object key) {
Node<K,V> e;
// 计算哈希值寻找结点,有就返回val,没有就返回null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
os:1.8真的很喜欢把赋值语句放在条件判断里…
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 // 数组长度大于0
&& (first = tab[(n - 1) & hash]) != null) { // 计算下标i,i号位不为空
if (first.hash == hash && // always check first node 首先检查头结点是否是目标结点,是就返回
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 如果头结点后连得第一个是树节点,调用树的get方法
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);
}
}
// 找不到就返回null
return null;
}
建议先把前面的扩容方式计算新下标的思路看一下
大概意思就是根据高位是0还是1来确定最后存的下标位置,0就保持原位,1就向后移一个旧长度的位置,这个结果和重新计算下标的结果是一致的
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* 初始化或翻倍数组。如果为空,初始化数组。
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 暂存旧table数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 暂存旧容量,如果是初始化调用扩容方法,旧容量为空,赋0
int oldThr = threshold; // 暂存旧阈值
int newCap, newThr = 0;
// 旧容量不为0
if (oldCap > 0) {
// 旧容量达到最大容量,不再扩容,将阈值提高至Integer.MAX_VALUE,有效阻止以后再度出现达到阈值的情况
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 左移翻倍容量和阈值(前提是不超过最大容量并且旧容量需要超出默认容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 旧容量为0,即table数组还未创建
// 为带参构造方法使用HashMap(int initialCapacity)及HashMap(int initialCapacity, float loadFactor)
// 因为初始化容量一开始是被存放在threshold中的
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 旧容量为0,即table数组还未创建
// 为无参构造方法使用HashMap(),使用默认值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 初始化阈值
}
// 如果新阈值为0
// 一种情况是使用了带参构造方法(else if (oldThr > 0))
// 另一种是旧容量未达到默认容量大小或翻倍后超出最大容量(else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
// oldCap >= DEFAULT_INITIAL_CAPACITY))
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; // 赋给全局变量
// 这里开始实施转移,transfer
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;
else if (e instanceof TreeNode) // 树节点,调用树的split方法
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 链表结点,保留之前的顺序(尾插法
else { // preserve order
// 第二层,链表遍历
// (我们以该链表原本位置在010(2),旧容量为100(4)举例子,分析已知原本的哈希值可能为010,可能为110
// head记录头结点,tail记录尾结点
Node<K,V> loHead = null, loTail = null; // lo表示low表示0,记录要转移的结点
Node<K,V> hiHead = null, hiTail = null; // hi表示high表示1,记录保留在原位置的结点
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { // 高位为0(010
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { // 高位为1(110
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 高位为0的链表,保持原位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位为1的链表,转移阵地
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新数组
return newTab;
}
数据结构改变
jdk1.7,数组+链表
jdk1.8,数组+链表+红黑树
链表节点插入
jdk1.7,头插法,再下移
jdk1.8,尾插法
扩容
扩容条件减少
jdk1.7,达到阈值并且新增结点发生碰撞才扩容,也就是说,如果达到阈值后,新增节点插入位置为空,则先不扩容
jdk1.8,只要达到阈值就扩容
扩容方式(针对链表而言)
jdk1.7,采取和插入时一致的方式,头插法,再下移
jdk1.8,拆分链表后直接移动头结点
扩容下标计算
jdk1.7,重新计算
jdk1.8,根据高位二进制决定,为0则下标不变,为1则往后移动一个旧数组长度的距离
简化了哈希算法
jdk1.7,使用了哈希种子,使用了四次扰动函数
jdk1.8,使用一次扰动函数