一、博客背景
hashmap在java7和java8中数据结构有了变化,我们本篇博客会根据我们目录中的提到的问题一起交叉查看jvav7和java8的源码,篇幅较多,请耐心看完,java7的版本号为1.7.0_04,java8的版本号为1.8.0_162
二、常见问题
- HashMap的底层原理是什么?
-
为什么JDK 7使用数组+链表?JDK8中为什么要使用红黑树?
-
hashmap底层是如何put的
-
hashmap底层是如何实现get的
-
hashmap如何扩容
-
什么是哈希冲突,hashmap又是如何解决的
-
jdk7中hashmap会死锁是什么原因?而java8如何解决死锁问题的
-
hashmap与hashtable的区别
-
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
-
为什么HashMap中String、Integer这样的包装类适合作为K?
-
谈一下HashMap的特性?
-
为什么是hashmap的默认长度16?HashMap 容量为2次幂的原因
-
java7,java8的hashmap有什么区别
-
java8中的扩容算法引入高低位链表的作用
-
jdk1.8中HashMap底层链表转红黑树的阈值为什么是8?红黑树转链表为什么是6?
-
HashMap、Hashtable、LinkedHashMap 和TreeMap之间的区别
Hashmap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。 HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 ConcurrentHashMap。
Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。
LinkedHashMap 是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
一般情况下,我们用的最多的是HashMap,在Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.
三、构造函数
1.java7
//定义的一些全局变量
//默认容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认承载系数
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
//下一个需要调整大小的大小值(容量*负载系数)
int threshold;
final float loadFactor;
//传入初始化大小和承载系数的构造函数
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
//留给子类实现的方法,hashmap中为空
init();
}
//传入map大小的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//空参构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
//含有其他集合强转hashmap的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}
private void putAllForCreate(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
//遍历传入集合的key和value来创建hashmap中的entry对象
putForCreate(e.getKey(), e.getValue());
}
private void putForCreate(K key, V value) {
//得到当前key对应的hash值,如果key是null,则hash值为0
int hash = (key == null) ? 0 : hash(key.hashCode());
//通过特定的算法返回hash值所在的索引,也就是在hashmap底层数组中所存的索引位置
int i = indexFor(hash, table.length);
/**
* Look for preexisting entry for key. This will never happen for
* clone or deserialize. It will only happen for construction if the
* input Map is a sorted map whose ordering is inconsistent w/ equals.
*/
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)))) {
//如果原先的hashmap中存在相同key,则将其对应的value用新值覆盖
e.value = value;
return;
}
}
//能走到这一步说明底层数组中不存在当前key,所以在table[i]所在的头部位置创建一个entry对象
createEntry(hash, key, value, i);
}
//创建entry对象
void createEntry(int hash, K key, V value, int bucketIndex) {
//获取原先数组table[bucketIndex]对应的entry
Entry<K,V> e = table[bucketIndex];
//以传入的参数创建新的entry,新的entry放在table[bucketIndex]的头部位置,并将下一个节点指向原先的entry
table[bucketIndex] = new Entry<>(hash, key, value, e);
//map的大小+1
size++;
}
//hashmap中的数组类型
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
2.java8
// HashMap初始化容量的默认大小(16),必须是2的整数次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
// HashMap初始化时的默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 将链表转为红黑树的阈值,当一个元素在被添加时,如果链表中的元素个数已经达到8个,则将链表转为红黑树形式
static final int TREEIFY_THRESHOLD = 8;
// 将红黑树转为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 链表被转化为红黑树时,哈希表最小的容量。为了避免冲突,该值至少为4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储链表的数组,table在第一次使用时会进行初始化,如果有必要会有resize的操作
// table的大小总是2的整数次幂
transient Node<K,V>[] table;
// 保存entrySet()方法的缓存,要和AbstractMap的keySet()和values()区分,AbstractMap有自己的Set集合,来缓存这两个方法的返回值
transient Set<Map.Entry<K,V>> entrySet;
// 键值对的个数
transient int size;
// HsahMap结构的修改次数,用于fail-fast机制
transient int modCount;
// 下一次resize的阈值大小 = HashMap容量 * 负载因子
int threshold;
//哈希表的负载因子
final float loadFactor;
// 参数指定了HashMap初始化时的容量以及负载因子
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;
//调用tableSizeFor计算出下次扩容阈值的大小,该阈值》=初始化容量的 2的n次方的值
this.threshold = tableSizeFor(initialCapacity);
}
// 参数指定了HashMap初始化时的容量,负载因子使用默认负载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 无参构造方法,默认的初始化容量为16,负载因子为默认的0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 根据其他Map来创建HashMap,负载因子为0.75
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
//如果table为0说明map为空
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
//返回一个新的阈值的 就得到了最近的大于t的2的整数次幂。
threshold = tableSizeFor(t);
}
//如果原先的map不为空,且传入map的大小大于原先map的扩容阈值,便扩容
else if (s > threshold)
//扩容
resize();
//扩容后,遍历 m 依次将元素加入当前表中。
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//存值,插入节点
putVal(hash(key), key, value, false, evict);
}
}
}
三、put函数
1.java7
public V put(K key, V value) {
//如果key为空,在table[0]的地方插入一个entry
if (key == null)
return putForNullKey(value);
//求出key的hash值
int hash = hash(key.hashCode());
//找到对应数组下标
int i = indexFor(hash, table.length);
// 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果有,直接覆盖,put 方法返回旧值就结束了
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//在table[i]的头部位置插入一个新的entry
addEntry(hash, key, value, i);
return null;
}
//在table[0]的地方插入一个entry
private V putForNullKey(V value) {
//遍历table[0]对应链表下的entry实例
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//存在key为null的直接覆盖旧值,并返回旧值结束
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//走到这一步,说明table[0]处不存在key为null的entry,则插入一个entry对象
addEntry(0, null, value, 0);
return null;
}
//插入一个entry对象
void addEntry(int hash, K key, V value, int bucketIndex) {
//获得原先位于table[bucketIndex]位置的元素
Entry<K,V> e = table[bucketIndex];
将新建实体置于table[bucketIndex]的头部位置
table[bucketIndex] = new Entry<>(hash, key, value, e);
//如果map大小大于扩容阈值,扩容
if (size++ >= threshold)
resize(2 * table.length);
}
2.java8
相对于java7,java8的put方法复杂了许多大致思路为
- 对key的hashCode()做hash,然后再计算桶的index;
- 如果没碰撞直接放到桶bucket里;
- 如果碰撞了,以链表的形式存在buckets后;
- 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树(若数组容量小于MIN_TREEIFY_CAPACITY,不进行转换而是进行resize操作)
- 如果节点已经存在就替换old value(保证key的唯一性)
- 如果表中实际元素个数超过阈值(超过load factor*current capacity),就要resize
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab存放 当前的哈希桶, p用作临时链表节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果当前哈希表是空的,代表是初始化
if ((tab = table) == null || (n = tab.length) == 0)
//那么直接去扩容哈希表,并且将扩容后的哈希桶长度赋值给n
n = (tab = resize()).length;
//如果当前index的节点是空的,表示没有发生哈希碰撞。 直接构建一个新节点Node,挂载在index处即可。
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//否则 发生了哈希冲突,p为table[i]中的第一个节点
Node<K,V> e; K k;
//如果p节点与插入节点的hash值相等,key相等,说明map中已经存在key对应的元素,直接将旧值覆盖即可
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将当前插入节点引用赋值给e
e = p;
else if (p instanceof TreeNode)
//是红黑树则放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//不是树节点,且发生hash碰撞说明,是链表节点
//遍历链表
for (int binCount = 0; ; ++binCount) {
//遍历到尾部,追加新节点到尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果追加节点后,链表数量》=8,则转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//转换为红黑树
treeifyBin(tab, hash);
break;
}
// 如果插入节点和原链表中的某个key具有相同的hash且key相同,则停止查找
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
//如果e不是null,表示在桶中找到key值、hash值与插入元素相等的结点,说明有需要覆盖的节点,
if (e != null) { // existing mapping for key
//则覆盖节点值,并返回原oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//这是一个空实现的函数,用作LinkedHashMap重写使用。
afterNodeAccess(e);
return oldValue;
}
}
//如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null。
//修改modCount
++modCount;
//更新size,并判断是否需要扩容。
if (++size > threshold)
resize();
//这是一个空实现的函数,
afterNodeInsertion(evict);
return null;
}
//z转化为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果table为空或者table数组太小,不满足转为红黑树的条件
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 对table数组进行扩容
resize();
// 如果符合转为红黑树的条件,且hash对应的数组位置不为null,即存在哈希值为hash的节点
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
// 遍历链表
do {
// 将Node节点转换为TreeNode节点
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
//重新排序形成红黑树
hd.treeify(tab);
}
}
代码看到这里,我们应该对于前面的几个问题已经有了答案了
Q1:HashMap的底层原理是什么?
在java7中是数组加链表,在java8中是数组加链表+红黑树(通过put方法节点超出时需要转化红黑树就可得知)
结构示意图
Q2;为什么JDK 7使用数组+链表?JDK8中为什么要使用红黑树?
- JDK 7使用数组+链表,是因为HashMap是根据Key计算Hash值,从而得到哈希表的索引下标,而哈希表本质是数组实现。当出现Hash冲突时,则一个桶可能需要存放多个数据。HashMap将会根据equals()方法,判断Hash冲突的Key是否是同一个值,此时如果仍然不相同,就会利用头插法,将出现Hash冲突的Key+Value存放在链表上。
- 但是如果一个链表比较长,那么查询的效率将会降低,所以JDK8中又使用了红黑树来解决链表过长导致查询效率变差的问题,会在一个桶上链表长度为8时,进行树化。但是树化的时候,会判断当前的长度是否小于64,如果小于,则不进行树化,而是选择进行一次扩容,因为扩容的时候会使哈希表长度增加,hash值会重新计算,将重新打乱当前的元素排列,分配到新的空间上,这样也避免了链表过长。
Q6:什么是哈希冲突,hashmap又是如何解决的
Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一钟
将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
哈希冲突就是当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
hashmap使用一下方法来解决哈希冲突
1. 使用链地址法(将数组和链表结合在一起)来链接拥有相同hash值的数据; 2. 使用hash函数来降低哈希冲突的概率,使得数据分布更平均;3. java8中引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
Q9:HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
hashCode()
方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()
计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
Q10:为什么HashMap中String、Integer这样的包装类适合作为K?
String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
- 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
- 内部已重写了
equals()
、hashCode()
等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;
如果我想要让自己的Object作为K应该怎么办呢?
重写hashCode()
和equals()
方法
- 重写
hashCode()
是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞; - 重写
equals()
方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;
Q11;HashMap的特性?
1.HashMap存储键值对实现快速存取,允许为null。key值不可重复,若key值重复则覆盖。
2.非同步,线程不安全。
3.底层是hash表,不保证有序(比如插入的顺序)
Q12:.为什么是hashmap的默认长度16?HashMap 容量为2次幂的原因
1.为了数据的均匀分布,减少哈希碰撞。因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次)
解释:因为无论是java7还是java8,在存放数据的时候,确定存放位置的都是用的:(n - 1) & hash
默认长度为16时,(n-1)为15,二进制为1111,这样进行&运算的时候(n - 1) & hash得到的就是hash值最后的4位数,假如这时hash值为8,那么与运算后 1111&1000 = 1000,数据存在第八位,然后又来一个hash为9的,与运算1111&1001 = 1001数据存在第9位
假如默认长度为15,那么n-1为14.二进制为1110,假如这时hash值为8,那么与运算后 1110&1000 = 1000,数据存在第八位,然后又来一个hash为9的,与运算1110&1001 = 1000数据还是存在第8位
可以很明显的发现,当长度15时,hash碰撞的概率明显增大。
Q3:hashmap底层是如何put的
通过前面的代码我们可以得出下面的流程图
简单的说就是
- (java8)考虑是否要初始化
- 根据key计算哈希值,找到hash表对应的索引
- 然后判断是否出现的hash冲突,如果没有则直接插入,查看是否需要扩容
- JDK1.7出现hash冲突直接插入链表中即可
- JDK1.8出现hash冲突,需要先判断当前是红黑树还是链表,对于链表可能会有一个树化的过程
四、扩容函数
1.java7
java7的扩容大致流程为
a)获取扩容前底层数组大小,如果大小以及等于map的最大容量,则将阈值修改为整数类型的最大值,然后返回
b)以扩容后的长度大小新建一个新的数组,将原数组的值迁移到新数组,map中原数组置空
C)将新数组指向map,并更新阈值
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新的数组
Entry[] newTable = new Entry[newCapacity];
// 将原来数组中的值迁移到新的更大的数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//HashMap的table属性引用新的Entry数组
table = newTable;
//更新阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable) {
//获取旧的Entry数组
Entry[] src = table;
int newCapacity = newTable.length;
//遍历旧的Entry数组
for (int j = 0; j < src.length; j++) {
//取得旧数组每个下标的元素
Entry<K,V> e = src[j];
if (e != null) {
//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
src[j] = null;
do {
Entry<K,V> next = e.next;
//重新计算每个元素在数组中的位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
2.java8
final Node<K,V>[] resize() {
//oldTab 为当前表的哈希桶
Node<K,V>[] oldTab = table;
//当前哈希桶的容量 length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//当前的阈值
int oldThr = threshold;
//初始化新的容量和阈值为0
int newCap, newThr = 0;
//如果当前容量大于0
if (oldCap > 0) {
//如果当前容量已经到达上限
if (oldCap >= MAXIMUM_CAPACITY) {
//则设置阈值是2的31次方-1
threshold = Integer.MAX_VALUE;
//同时返回当前的哈希桶,不再扩容
return oldTab;
}//否则判断新的容量是否小于最大值上限,并且旧的容量已经大于等于默认初始容量16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//那么新的阈值也等于旧的阈值的两倍
newThr = oldThr << 1; // double threshold
}//如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;//那么新表的容量就等于旧的阈值
else {//如果当前表是空的,而且也没有阈值。代表是初始化时没有任何容量/阈值参数的情况 // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;//此时新表的容量为默认的容量 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的阈值为默认容量16 * 默认加载因子0.75f = 12
}
if (newThr == 0) {//如果新的阈值是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) {
//取出当前的节点 e
Node<K,V> e;
//如果当前桶中有元素,则将链表赋值给e
if ((e = oldTab[j]) != null) {
//将原哈希桶置空以便GC
oldTab[j] = null;
//如果当前链表中就一个元素,(没有发生哈希碰撞)
if (e.next == null)
//直接将这个元素放置在新的哈希桶里。
//注意这里取下标 是用 哈希值 与 桶的长度-1 。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
newTab[e.hash & (newCap - 1)] = e;
//如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
else { // preserve order
//因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量
//低位链表的头结点、尾节点
Node<K,V> loHead = null, loTail = null;
//高位链表的头节点、尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;//临时节点 存放e的下一个节点
do {
next = e.next;
// 这里又是一个利用位运算 代替常规运算的高效点:将同一链表中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
// 若(e.hash & oldCap)为0,则该节点在新table中的下标不变
// 若(e.hash & oldCap)不为0,则该节点在新table中的下标变为j + oldCap
if ((e.hash & oldCap) == 0) {
//给头尾节点指针赋值
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}//高位也是相同的逻辑
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}//循环直到链表结束
} while ((e = next) != null);
//将低位链表存放在原index处,
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//将高位链表存放在新index处
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
java8扩容机制流程图如下
现在可以得到我们Q5和Q7的答案了
Q5:hashmap如何扩容
第一,对hashmap底层数组大小进行扩容,扩容后的大小为当前的两倍。第二则是重新计算存放位置,对于java7而言,原先某个数组下标的链表元素会反转,而对于java8而言,则可能是扩容前存在在同一个下标位置的链表元素,扩容后有可能存在不同的下标位置去。还有就是可能会会转化为红黑树节点
Q7:jdk7中hashmap会死锁是什么原因?而java8如何解决死锁问题的
在多线程下,进行 put 操作会导致 HashMap 死循环,原因在于 HashMap 的扩容 resize()方法。由于扩容是新建一个数组,复制原数据到数组。由于数组下标挂有链表,所以需要复制链表,遍历一个节点就插入一个节点到新的哈希桶数组,这样遍历完之后就会导致链表数据反转过来,而就是这样的操作会在多线程情况下有可能导致环形链表。但 JDK1.8 中是采用两个索引结点共同来保持旧链表的引用,直到该索引处对应的链表全部遍历完之后,再其中的含有旧所有结点指向放在新的哈希桶数组对应的位置。而不是遍历一个节点就插入一个节点到新的哈希桶数组。所以不会出现死链。
五、get方法
1.java7
public V get(Object key) {
if (key == null)
//key 为 null 的话,会被放到 table[0],所以只要遍历下 table[0] 处的链表就可以了
return getForNullKey();
//计算存储的下标位置
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//有相同key的直接返回value值
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
//获取table[0] 处key为null的值
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
2.java8
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// table不为null且长度不为0,且存在哈希值为hash的节点,first为对应下标的首节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 总是先检查first节点是否符合条件,若符合,则直接返回first节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 否则,若首节点存在后继节点
if ((e = first.next) != null) {
// 若首节点是TreeNode类型节点,则从红黑树中查找节点
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;
}
看了上面的get方法,我们可以得到Q4的答案了
Q4:hashmap底层是如何实现get的
相对于 put 过程,get 过程是非常简单的。
- 根据 key 计算 hash 值。
- 找到相应的数组下标
- JAVA7只需遍历该数组位置处的链表,直到找到相等(==或equals)的 key对应的value,而java8还需要判断是否为树节点,为树节点从树中取出数据直接返回
六、remove方法
1.java7
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
// 1. 计算hash值
int hash = (key == null) ? 0 : hash(key.hashCode());
// 2. 计算存储的数组下标位置
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数组中的元素(即链表的头结点)
// 则删除操作 = 将头结点的next引用存入table[i]中
if (prev == e)
table[i] = next;
//否则 将以table[i]为头结点的链表中,当前Entry的前1个Entry中的next 设置为 当前Entry的next(即删除当前Entry = 直接跳过当前Entry)
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
2.java8
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// matchValue如果为true,则表示删除一个node的条件是:key和value都一致才删除
// movable如果为false,则表示删除当前节点时,不会移动其它节点
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;
// table不为null且长度不为0,且存在哈希值为hash的节点,p为对应下标的首节点
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;
// 总是先检查首节点p是否符合条件,若符合,则node = p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 否则,若首节点存在后继节点
else if ((e = p.next) != null) {
// 若首节点是TreeNode类型节点,则从红黑树中查找节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 从链表中查找节点
else {
do {
// 若找到符合条件的节点,则node = e,退出循环
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
// 更新p
p = e;
} while ((e = e.next) != null);
}
}
// 如果找到指定key与哈希值的node,且保证了删除策略matchValue,则可以删除
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// node为红黑树节点,则调用红黑树节点删除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 否则,node节点是链表节点,若node == p,p为node的前驱节点,则说明node为首节点,直接更新数组对应下标的首节点
else if (node == p)
tab[index] = node.next;
// 否则,更新p.next = node.next,删除node节点
else
p.next = node.next;
// HashMap结构修改次数加1
++modCount;
// 元素个数减1
--size;
afterNodeRemoval(node);
// 返回删除节点
return node;
}
}
// 返回null
return null;
}
//从哈希表中删除某个节点, 如果参数matchValue
是true,则必须key 、value都相等才删除。
//如果movable
参数是false,在删除节点时,不移动其他节点
Q13:java7,java8的hashmap有什么区别
1.JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
2.扩容后的元素存储的计算方式不一样,在JDK1.7的时候是直接用hash值和需要扩容的大小进行&(int i = indexFor(e.hash, newCapacity);)而1.8是原map中存储位置或者原map存储位置加上扩容前的旧容量
3.JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构
Q14:java8中的扩容算法引入高低位链表的作用
在java8中扩容后因素的存储位置要么是原map中存储位置或者原map存储位置加上扩容前的旧容量,所以java8引入了高低位链表,地位链表的数据存储下标直接为原存储位置,而高位链表的存储位置则是为原位置加上扩容前map大小。引入高低链后就省去了(n - 1) & hash求元素下标位置这一步提高了性能。
具体的分析可查看下别人的博客:https://blog.csdn.net/qq32933432/article/details/86668385
Q15:jdk1.8中HashMap底层链表转红黑树的阈值为什么是8?红黑树转链表为什么是6?
和hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所
以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,红黑树转链表的阈值为6,主要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停相互激荡转换,白白浪费资源。
Q8:hashmap与hashtable的区别
1、继承的父类不同:Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
2、线程安全性不同:Hashtable是线程安全,而HashMap则非线程安全。Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap
3、HashMap可以使用null作为key,不过建议还是尽量避免这样使用。HashMap以null作为key时,总是存储在table数组的第一个节点上。而Hashtable则不允许null作为key。
4、内部实现使用的数组初始化和扩容方式不同:HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
Q16:HashMap、Hashtable、LinkedHashMap 和TreeMap之间的区别
Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复。
Hashmap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。 HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 ConcurrentHashMap。
Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。
LinkedHashMap 是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
一般情况下,我们用的最多的是HashMap,在Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.