HashMap的结构是什么
-
JDK1.7
底层使用数组+链表,维护了一个叫做Entry的内部类
-
JDK1.8
底层使用数组+链表+红黑树结构,维护了Node(链表)类,实现了Map.Entry接口,TreeNode(红黑树),继承了LinkedHashMap.Entry
- Node类
static class Node<K,V> implements Map.Entry<K,V> { //Key计算的hash值 final int hash; //Key final K key; //Value V value; //链表使用 Node<K,V> next; }
- TreeNode类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //头节点 TreeNode<K,V> parent; //左子树 TreeNode<K,V> left; //右子树 TreeNode<K,V> right; TreeNode<K,V> prev; boolean red; }
HashMap的默认大小是多少
默认大小为16(1 << 4)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
为什么默认大小的写法是1 << 4,而不是直接写16?
- 因为位运算比数计算的效率高
为什么默认大小是16?
- 因为在使用2的幂数字时,Length - 1的二进制值全部为1,此时index的结果就等同于hashcode的后几位的值,当输入值本身的hashcode分布平均,则hash算法的分布就是平均的
- 这样是为了实现均匀分布
为什么HashMap要重写HashCode方法?
- 因为当map插入的时候,是根据key的hash计算出的index来插入的,如果两个key的index都为同样的,此时就形成了链表,如果不重写hashcode方法,那就无法保证同一个链表上的hashcode不同,那样在get的时候就会出现问题。
- 重写hashcode方法,保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。
HashMap的插入方式是什么
数组插入方式
每次进行put的时候,会运行一个hash算法
-
hash算法
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
根据这个hash算法得到一个index值,根据index的值进行插入,如果数组中index的值已经有了数据,那么就会在index值的位置形成链表
-
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;
//第一次运行,使用resize()进行数组初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//数组中没有节点,直接插入
//newNode创建一个新的node节点
tab[i] = newNode(hash, key, value, null);
else {//当前index有值,转换为链表/红黑树
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//hash相同,且key相同
//替换原节点
e = p;
else if (p instanceof TreeNode) //当前是红黑树结构
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //当前是链表
//遍历链表,采取尾插法
for (int binCount = 0; ; ++binCount) {
//循环到最后一个节点,插入新元素
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//TREEIFY_THRESHOLD = 8 链表转换红黑树条件
//如果当前的循环次数大于等于7,就将链表转换为红黑树
//如果是TREEIFY_THRESHOLD - 1 = -1,则默认为1
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//Key相同,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//保存当前节点,为次循环做准备
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果当前集合中的元素大于12个,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
链表插入方式
-
JDK1.7
在jdk1.7及以前的版本,采用头插法,新值插入到头部,原有的值插入到新值的next节点,形成新链表
-
JDK1.8
在jdk1.8中,改为了尾插法
为什么采用了尾插法
- 使用头插法,每次插入都会改变链表的顺序,在扩容的时候,可能会出现环形链表,在多线程中,可能会出现死循环
- 使用尾插法,保证了链表元素原本的顺序
HashMap取值
-
通过传入的key计算出key的hash值,利用hash值去数组中寻找
-
getNode()
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //当前table不是空,且当前tab的长度大于0,且当前hash计算出的index,并取得当前index的值 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //如果当前index的数据的hash与传入的相等,则直接返回 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; //此时为index取得的值可能为链表或红黑树 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; }
HashMap扩容
HashMap什么时候扩容
-
默认大小
//1 << 4 = 16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
-
负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
当元素存储到13个(16 * 0.75 = 12)的时候,进行扩容
HashMap怎么进行扩容
-
1.创建新的空数组,大小为原数组的2倍大小
-
2.遍历原数组,将元素组的数据重新Hash到新数组
-
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; //如果oldCap大于0,表示为扩容 if (oldCap > 0) { //MAXIMUM_CAPACITY = 1 << 30 (1073741824) map存储最大值 //Integer.MAX_VALUE = 0x7fffffff //如果大于最大值,则将返回旧的数组 if (oldCap >= MAXIMUM_CAPACITY) { 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) newCap = oldThr; //此时是初始化,使用默认构造方法 else { //使用空构造方法,则设置默认值 //数组大小设置为16 newCap = DEFAULT_INITIAL_CAPACITY; //数组扩容点设置为16*0.75,为12 newThr = (int)(DEFAULT_LOAD_FACTOR * 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; //初始化数组或创建扩容后的新数组 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) //e.hash & (newCap - 1)计算数据在新数组中的index newTab[e.hash & (newCap - 1)] = e; //此时为红黑树 else if (e instanceof TreeNode) //调用TreeNode中的split方法 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //此时为链表 else { //初始化低位头尾节点 Node<K,V> loHead = null, loTail = null; //初始化高位头尾节点 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; //链表循环 do { next = e.next; //计算位置 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); //低位置的节点不是null,放入低位置 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //高位置的节点不是null,放入高位置 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
为什么要重新刷新原数组的hash
- 因为数组扩容后,Length-1就发生了改变,hash计算的规则也发生了改变,所以重新刷新
HashMap构造方法
/**
* 默认构造方法(空)
*/
public HashMap() {
//DEFAULT_LOAD_FACTOR = 0.75f
//未传参数,则负载因子设置为默认值
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
/**
* 构造方法(初始化Map大小)
* @param:initialCapacity(Map数组大小)
*/
public HashMap(int initialCapacity) {
//若初始化时传递了Map的大小,设置map的大小为initialCapacity,负载因子为默认值
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 构造方法(初始化Map大小,负载因子)
* @param:initialCapacity(Map数组大小)
* @param:loadFactor(负载因子)
*/
public HashMap(int initialCapacity, float loadFactor) {
//如果map的大小小于0,则直接抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//MAXIMUM_CAPACITY = 1073741824
//如果map的大小大于设置的最大值
if (initialCapacity > MAXIMUM_CAPACITY)
//让map的大小等于最大值
initialCapacity = MAXIMUM_CAPACITY;
//如果负载因子小于等于0或负载因子为非法数值,直接抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
//设置负载因子
this.loadFactor = loadFactor;
//设置扩容参数
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 返回大于输入参数且最接近的2的幂数
* @param:cap
*/
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/**
* 根据传入的Map初始化HashMap
* @param: Map<? extends K, ? extends V> m
*/
public HashMap(Map<? extends K, ? extends V> m) {
//设置默认负载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* 根据传入的Map初始化HashMap
* @param:cap
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取Map里的参数个数
int s = m.size();
//如果s大于0,才进行初始化
if (s > 0) {
//如果Node数组为空
if (table == null) {
float ft = ((float) s / loadFactor) + 1.0F;
int t = ((ft < (float) MAXIMUM_CAPACITY) ?
(int) ft : MAXIMUM_CAPACITY);
if (t > threshold) {
//设置数组扩容值
threshold = tableSizeFor(t);
}
} else if (s > threshold) { //如果s的个数大于扩容值
//数组扩容
resize();
}
//数组初始化
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);
}
}
}
HashMap删除元素
/**
* Map删除元素
*/
public V remove(Object key) {
Node<K, V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* Map删除元素的真正方法
* @param: matchValue 如果为true,只在值相等的时候删除
* @param: 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;
//如果当前的map不为空,且长度大于0
if ((tab = table) != null && (n = tab.length) > 0 &&
//(index = (n - 1) & hash) -> 确定当前的index值,等同于putVal()中的
//当前key去出的值不为空
(p = tab[index = (n - 1) & hash]) != null) {
Node<K, V> node = null, e;
K k;
V v;
//p为当前Key取出的Node对象
//这里的第一个if为数组情况
//判断p取出来的Key与传入的Key是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) {
//node为临时变量
node = p;
} else if ((e = p.next) != null) {//这里说明不在数组中,向下在链表/红黑树寻找
//此时为红黑树
if (p instanceof TreeNode) {
//node赋值
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
} else { //此时为链表
//链表循环
do {
//e为链表循环的临时变量
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))){
//找到参数,跳出循环
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//!matchValue = true
//node为要删除的元素
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;
} else { //此时为链表
p.next = node.next;
}
//HashMap修改次数+1
++modCount;
//HashMap中元素-1
--size;
//空方法
afterNodeRemoval(node);
return node;
}
}
return null;
}
HashMap其他方法
/**
* 判断当前的Key是否存在于Map中
* @param: key
*/
public boolean containsKey(Object key) {
//根据Key的Hash值与Key值查找
return getNode(hash(key), key) != null;
}
/**
* Map清空
*/
public void clear() {
Node<K, V>[] tab;
//Map操作次数+1
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
//置空数组
for (int i = 0; i < tab.length; ++i) {
tab[i] = null;
}
}
}
/**
* Map遍历 JDK1.8后新增
* 使用了Consumer -> 消费型接口
*/
public final void forEach(Consumer<? super K> action) {
Node<K, V>[] tab;
if (action == null) {
throw new NullPointerException();
}
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (Node<K, V> e : tab) {
for (; e != null; e = e.next) {
action.accept(e.key);
}
}
if (modCount != mc) {
throw new ConcurrentModificationException();
}
}
}
HashMap总结
HashMap的底层数据结构?
- JDK1.7:数组+链表
- JDK1.8:数组+链表+红黑树
HashMap的存取原理?
- 每次put的时候调用hash方法,利用key的hashcode计算出数组存放的index,当数组大于12时扩容,若当前index有值,则转为链表,元素插入链表尾部,当链表元素大于7时,转为红黑树。
Java7和Java8的区别?
- 链表插入从头插法转换为尾插法
- 优化了链表,新增红黑树的数据结构
为啥会线程不安全?
- 操作元素的put和get方法都没有加锁,在多线程同时操作的情况下,无法保证get出的值就是之前put的值
有什么线程安全的类代替么?
- HashTable 方法上加锁,效率低(悲观锁)
- ConcurrentHashMap 代码中加锁,效率高(乐观锁)
默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?
- 默认大小16
- 因为16属于折中方案
- 因为使用2的幂次数可以保证后几位都为(0000),保证了数据均匀分配
HashMap的扩容方式?负载因子是多少?为什是这么多?
- 创建二倍原数组大小的新数组,复制原数组至新数组
- 0.75f
- 提高空间利用率,减少查询成本,hash碰撞最小
- 加载因子过高(1):减少空间开销,提高空间利用率,增加查询成本
- 加载因子过低(0.5):减少查询承包,降低了空间利用率,加大了扩容几率
HashMap的主要参数都有哪些?
-
初始默认大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //16
-
数组最大大小
static final int MAXIMUM_CAPACITY = 1 << 30; //1073741824
-
负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
-
链表转红黑树大小
static final int TREEIFY_THRESHOLD = 8;
-
红黑树转链表大小
static final int UNTREEIFY_THRESHOLD = 6;
-
树转换最小值(当表中元素大于此值,才可以转换为树,否则进行扩容而不是树化)
static final int MIN_TREEIFY_CAPACITY = 64;
-
HashMap修改次数
//当循环map的时候,modCount与expectedModCount不相等,则直接抛出异常 //针对于并发问题,使用fail-fast策略 transient int modCount;
什么是hash碰撞?
- 当两个不同的元素计算出的index值相同,此时称为hash碰撞
HashMap是怎么处理hash碰撞的?
-
在hash算法中有以下几步操作
- 计算出key的hashcode(h = key.hashCode())
- h的二进制右移16位(h >>> 16)
- h原值与右移后的数据进行异或运算(h ^ (h >>> 16))
- 结果等于数组大小 - 1 和第三步的结果进行与运算(index = (n - 1) & (h ^ (h >>> 16)))
-
hash()核心代码
static final int hash(Object key) { int h; //这里是第三步 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
确定index的代码
//这里的n-1为数组大小 - 1,例如当前数组大小为16,此处就为16-1=15 //这里的hash为上面代码计算出的hash值 tab[i = (n - 1) & hash]
hash的计算规则?
- 计算出key的hashcode,令此值的前后16位进行异或运算
当HashMap到达扩容临界点,两个线程同时到达,此时HashMap会发生什么
- 两个线程分别进行扩容,当扩容完成后,会形成一个环形链表
- 当此时,对环形链表的位置进行一个Get操作,且Get的Key值是不存在的,那么就会出现死循环,形成死锁