特点
底层是数组+链表/红黑树,无序,非线程安全
- key:可为null,不可重复,底层使用Set存储,所在类需重写
equals
和hashcode
。 - value:可为null,可重复,底层使用Collection存储,所在类需重写equals。
- entry:底层使用Set存储,相当于 key-value 键值对。
HashMap
实行了懒加载, 新建HashMap
时不会对table
进行赋值, 而是到第一次
插入时, 进行resize
时构建table
;
成员变量
-
默认初始容量
(必须为2的整数幂)static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
-
最大容量
,当指定值较高将被其替换(必须为2的整数幂)static final int MAXIMUM_CAPACITY = 1 << 30;
-
默认加载因子
(未被指定时使用)static final float DEFAULT_LOAD_FACTOR = 0.75f;
为什么是0.75?
根据统计学的结果, hash冲突是符合泊松分布的, 而冲突概率最小的是在7-8之间, 都小于百万分之一了; 所以HashMap.loadFactor选取只要在7-8之间的任意值即可, 但是为什么就选了3/4这个值, 我们看了HashMap的扩容机制也就知道了; -
树化阈值
,当链表达到此值将转为树(至少为8)static final int TREEIFY_THRESHOLD = 8;
-
链表阈值
,当树容量小于此值转为链表static final int UNTREEIFY_THRESHOLD = 6;
-
树化时数组最小容量
,当即将树化时若数组未达到此阈值,将进行resize扩容static final int MIN_TREEIFY_CAPACITY = 64;
-
存放数据的数组
transient Node<K,V>[] table;
-
保留缓存的entrySet()
transient Set<Map.Entry<K,V>> entrySet;
-
此映射中包含的键值映射数。
transient int size;
-
此HashMap在结构上被修改的次数结构修改是指那些更改HashMap中映射数量或以其他方式修改其内部结构(例如,重新刷新)的次数。
transient int modCount;
-
要调整大小的下一个大小值(容量*负载系数)。
int threshold;
-
加载因子
final float loadFactor;
构造函数
无参
构造一个空集合,初始容量为16,加载因子为0.75
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);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
常用方法
-
put(Object key,Object value):添加,key已存在则value覆盖
-
remove(Object key):删除
-
get(Object key):查询
- 首先获取当前key对应的数组索引位置,然后判断该位置的首节点是否是自己想要的值根据key和key.hashCode()来判断
- 首节点如果不是的话,判断节点是否是树节点,如果是的话,通过调用getTreeNode()来实现get()方法,如果不是树节点,那么就是链表,然后死循环遍历链表,查询是否有自己想要的值
- 如果上面的步骤都没有查询到数据,直接返回null.
public V get(Object key) { //定义一个Node对象来接收 Node<K,V> e; //调用getNode()方法,返回值赋值给e,如果取得的值为null,就返回null,否则就返回Node对象e的value值 return (e = getNode(hash(key), key)) == null ? null : e.value; }
-
size():返回长度
-
keySet():遍历key
-
value():返回所有value
-
entrySet():返回所有键值对
public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }
添加流程:
- 调用key所在类计算哈希值
内部类
Node
链表节点
TreeNode
树节点
Final方法
putTreeVal
-
从root节点开始寻找,
若 预节点hash < 当前节点的 hash
到左树寻找
否则 预节点hash > 当前节点的 hash
右树寻找
否则 相同节点(同对象 或 同值) 直接返回。
-
若 hash相同 但是 equal不同
若比较Comparable接口相同
则在左右子树递归的寻找是否有与要插入的key equals相同的元素。如果有那么直接return返回。
(也即是没实现Comparable接口,大小由hash判定。实现了,则由Comparable接口的比较方法判定) -
如果遍历完所有的节点 并未找到equals相同的节点。那就需要插入该新节点。必须分出大小,所以通过执行tieBreakOrder方法,该方法的返回值是-1,1。如果是-1则插入到左边节点,1就插入到右边节点。
-
插入完成之后,需要重新移动root节点 到table数组的i位置的第一个节点上 并且需重新平衡红黑树。
final TreeNode<K,V> putTreeVal(HashMap<K,V> map/*当前Hashmap对象*/, Node<K,V>[] tab/*table数组*/,int h/*hash值*/, K k, V v) {
Class<?> kc = null;
boolean searched = false; //标识是否被检索过
TreeNode<K,V> root = (parent != null) ? root() : this; // 找到root根节点
for (TreeNode<K,V> p = root;;) { //从根节点开始遍历循环
int dir, ph; K pk;
// 根据hash值 判断方向
if ((ph = p.hash) > h)
// 大于放左侧
dir = -1;
else if (ph < h)
// 小于放右侧
dir = 1;
// 如果key 相等 直接返回该节点的引用 外边的方法会对其value进行设置
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
/**
*下面的步骤主要就是当发生冲突 也就是hash相等的时候
* 检验是否有hash相同 并且equals相同的节点。
* 也就是检验该键是否存在与该树上
*/
//说明进入下面这个判断的条件是 hash相同 但是equal不同
// 没有实现Comparable<C>接口 或者 (实现该接口 并且 k与pk Comparable比较结果相同)
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//在左右子树递归的寻找 是否有key的hash相同 并且equals相同的节点
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
//找到了 就直接返回
return q;
}
//说明红黑树中没有与之equals相等的 那就必须进行插入操作
//打破平衡的方法的 分出大小 结果 只有-1 1
dir = tieBreakOrder(k, pk);
}
//下列操作进行插入节点
//xp 保存当前节点
TreeNode<K,V> xp = p;
//找到要插入节点的位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
//创建出一个新的节点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
//小于父亲节点 新节点放左孩子位置
xp.left = x;
else
//大于父亲节点 放右孩子位置
xp.right = x;
//维护双链表关系
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//将root移到table数组的i 位置的第一个节点
//插入操作过红黑树之后 重新调整平衡。
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
Static utilities
hash
计算hash值。
static final int hash(Object key) {
int h;
/* if(key == null)
return 0
else
低16位与他的高16位做异或运算
Tip:如果不这样的话,那么就只有hash()返回值的末x位参与到运算,这样就会造成hash冲突的概率高一些。如果先把key的hashCode()返回值的高16位和低16位进行异或运算,这样高16位也参与到hash()的运算逻辑了,这样就能减少冲突。*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
comparableClassFor
当在红黑树中添加节点时,会根据hash值进行比较,大右小左,但如果hash值相同呢?
这时还是会尝试使用compareComparables
方法进行比较,但要保证参与比较的类实现了Comparable
接口,此操作由本方法完成。
Eg:
入参 | 出参 | 状态 |
---|---|---|
x implement Comparable | x.class | T |
x | null | F |
String | String.class | T |
static Class<?> comparableClassFor(Object x) {
/*if (类 x 实现了Comparable接口*/
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
/*if (类 x 是String类型)返回String.class*/
if ((c = x.getClass()) == String.class) // bypass checks
return c;
/*if (类 x 实现了接口)*/
if ((ts = c.getGenericInterfaces()) != null) {
for (int i = 0; i < ts.length; ++i) { //迭代接口
//if (x.class实现的接口有参数(Eg:implement I<C>) && */
//参数化类型的接口为Comparable类型(Eg:implement Comparable<C>)&&
/*参数不为空 && 参数有一个 && 其为 x.class*/
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
//x implement Comparable<x>
return c;
}
}
}
return null;
}
compareComparables
当在红黑树中添加节点时,会根据hash值进行比较,大右小左,但如果hash值相同呢?这时还是会尝试使用
compareComparables
方法进行比较。
/**
* @Param kc:kc Class对象
* @Param k:当前节点对象
* @Param x:新节点对象
*/
static int compareComparables(Class<?> kc, Object k, Object x) {
/*
if 新节点不为空 || 新节点的Class对象 与 kc 不同
return 0;
else
return 新节点.compareTo(当前节点) Tip:a.compareTo(b) === a - b
*/
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
tableSizeFor
将传入容量转换为 2 的幂次方
static final int tableSizeFor(int cap) {//入参:148(10010100)
int n = cap - 1;
n |= n >>> 1; //n = n | (n >>> 1) 1xxxxxxx | 01xxxxxxx :11xxxxxx 128 + 64 + x = 192 + x
n |= n >>> 2; //n = n | (n >>> 2) 11xxxxxx | 0011xxxx :1111xxxx 128 + 64 + 32 + 16 + x = 240 + x
n |= n >>> 4; //n = n | (n >>> 4) 1111xxxx | 00001111 :11111111 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255
n |= n >>> 8; //n = n | (n >>> 8) 11111111 | :11111111 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255
n |= n >>> 16; //n = n | (n >>> 16) 11111111 | :11111111 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255
/*
if 255 < 0
return 1
else
if 255 >= MAXIMUM_CAPACITY
return MAXIMUM_CAPACITY
else
return 255+1(出参:256(11111111 + 1) )
*/
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
Eg
入参:148(10010100) 出参:256(11111111 + 1)
Final方法
putMapEntities
将一个map赋值给新的HashMap
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取传入的map集合的大小
int s = m.size();
if (s > 0) {
//如果HashMap没有被初始化
if (this.table == null) {
//将s除以负载因子+1可以得到HashMap所需的最大负载容量
float ft = (float)s / this.loadFactor + 1.0F;
//如果计算得到的最大负载容量大于最大值,则将t赋值为最大值
int t = ft < 1.07374182E9F ? (int)ft : 1073741824;
//如果t大于当前最大负载容量,则进行调整
if (t > this.threshold) {
this.threshold = tableSizeFor(t);
}
}
//如果table已经被初始化且传入map的大小大于当前的最大负载容量则开始调整HashMap的大小
else if (s > this.threshold) {
this.resize();
}
//获取传入map的迭代器
Iterator var8 = m.entrySet().iterator();
//将map中的元素逐一添加到HashMap中
while(var8.hasNext()) {
Entry<? extends K, ? extends V> e = (Entry)var8.next();
K key = e.getKey();
V value = e.getValue();
this.putVal(hash(key), key, value, false, evict);
}
}
}
resize
扩容
- 如果 table == null, 则为HashMap的初始化, 生成空table;
- 如果table不为空, 需要重新计算table的长度, newLength = oldLength << 1(注, 如果原oldLength已经到了上限, 则newLength = oldLength);
- 遍历oldTable
- 首节点为空, 本次循环结束;
- 无后续节点, 重新计算hash位, 本次循环结束;
- 当前是红黑树, 走红黑树的重定位;
- 当前是链表, JAVA7时还需要重新计算hash位, 但是JAVA8做了优化, 通过(e.hash & oldCap) == 0来判断是否需要移位; 如果为真则在原位不动, 否则需要移动到当前hash槽位 + oldCap的位置;
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) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
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;
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;
@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;
else if (e instanceof TreeNode) //如果头节点为红黑树类型,调用split
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 执行到这里,桶为链表
/* 正常情况下,计算节点在table中的下标的方法是:hash & (oldTable.length - 1)扩容之后,table长度
翻倍,计算table下标的方法是hash & (newTable.length - 1)也就是hash & (oldTable.length * 2 - 1)
于是得出结论:新旧两次计算下标的结果,要么相同,要么是新下标等于旧下标加上旧数组的长度。*/
Node<K,V> loHead = null, loTail = null;/*下标不变*/
Node<K,V> hiHead = null, hiTail = null;/*下标改变*/
Node<K,V> next;
do { //循环链表
next = e.next; //为 next 赋值下一节点
if ((e.hash & oldCap) == 0) { //如果新链表最高位为0,下标不变
if (loTail == null) //尾空表示链表为空,e 做头
loHead = e;
else //链表不为空,e 置于最后
loTail.next = e;
loTail = e; //e 做尾
}//下同
else { //如果新链表最高位为1,下标改变
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; //直接将头节点置于新链表同下标
}
if (hiTail != null) { //下标改变链表不为空
hiTail.next = null;
newTab[j + oldCap] = hiHead;//直接将头节点置于新链表【旧下标 + 旧容量】
}
}
}
}
}
return newTab;
}
putVal
- 如果key对应的索引位置是null,那么直接插入
- 数组里面key对应的索引值位置的值不为null,判断这个老值的key是否和新put的key是否相同,如果相同,就把老的值返回,并且记录这个位置
- 数组里面key对应的索引值位置的值不为null,判断这个索引位置的值是不是树结构,如果是树结构,调用树结构putTreeVal方法添加数据
- 数组里面key对应的索引值位置的值不为null,然后这个索引位置的值就是一个链表结构,然后遍历所有的链表(当遍历的长度大于8的时候,就会转成树结构),如果链表结构里面有key值和新key值相同,就把老的值给返回,并且记录这个位置,如果遍历到尾部还不相同,那么就使用尾插入把数据给添加进去。
- 对2步骤和4步骤记录的位置进行处理,一是把标记的位置的老值给返回,二是把新插入的值放到标记的位置上面。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果table为空,或者还没有元素时,扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果首结点值为空,则创建一个新的首结点。
// 注意:(n - 1) & hash才是真正的hash值,也就是存储在table位置的index。在1.6中是封装成indexFor函数。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 执行到这儿说明碰撞了
Node<K,V> e; K k;
// 如果在首结点与我们待插入的元素有相同的hash和key值,则先记录。
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);
else { // 到这一步,说明首结点类型为链表类型。
for (int binCount = 0; ; ++binCount) {
// 如果遍历到末尾时,先在尾部追加该元素结点。
if ((e = p.next) == null) {//#1 //下一节点为空,可以插入
p.next = newNode(hash, key, value, null);
// 当遍历的结点数目大于8时,则采取树化结构。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果找到与我们待插入的元素具有相同的hash和key值的结点,则停止遍历。此时e已经记录了该结点
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是替换,true是不替换
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 这个是空函数,可以由用户根据需要覆盖
return oldValue;
}
}
++modCount;
// 当结点数+1大于threshold时,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 这个是空函数,可以由用户根据需要覆盖
return null;
}
getNode
final Node<K,V> getNode(int hash, Object key) {
//定义几个变量
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//首先是判断数组table不能为空且长度要大于0,同时把数组长度tab.length赋值给n
if ((tab = table) != null && (n = tab.length) > 0 &&
//其次是通过[(n - 1) & hash]获取key对应的索引,同时数组中的这个索引要有值,然后赋值给first变量
(first = tab[(n - 1) & hash]) != null) {
//这个first其实就是链表头的节点了,接下来判断first的hash值是否等于传进来key的hash值
if (first.hash == hash &&
//再判断first的key值赋值给k变量,然后判断其是否等于key值,或者判断key不为null时,key和k变量的equals比较结果是否相等
((k = first.key) == key || (key != null && key.equals(k))))
//如果满足上述条件的话,说明要找的就是first节点,直接返回
return first;
//走到这步,就说明要找的节点不是首节点,那就用first.next找它的后继节点 ,并赋值给e变量,在这个变量不为空时
if ((e = first.next) != null) {
//如果首节点是树类型的,那么直接调用getTreeNode()方法去树里找
if (first instanceof TreeNode)
//这里就不跟进去了,获取树中对应key的节点后直接返回
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//走到这步说明结构还是链表
do {
//这一步其实就是在链表中遍历节点,找到和传进来key相符合的节点,然后返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
//获取e节点的后继节点,然后赋值给e,不为空则进入循环体
} while ((e = e.next) != null);
}
}
//以上条件都不满足,说明没有该key对应的数据节点,返回null
return null;
}
附录
Type(接口)
- 原始类型
- 参数化类型(ParameterizedType)
- 数组类型
- 类型变量
ParameterizedType(接口)
ParameterizedType extendsType
参数化类型
getActualTypeArguments
Type[] getActualTypeArguments()
返回实际类型的数组
Eg:
入参:Food<Fruit,Vegetable> 出参:[Fruit,Vegetable]
getRawType
Type getRawType()
该方法返回此类型的类或接口的类型
Eg:
入参:List 出参:List;入参:Map<String, Object> 出参:Map。
getOwnerType
Type getOwnerType()
返回一个Type类型对象,表示该类型所属的类型,必须至少有两个类型
Eg:
入参O<T>.I<S>,出参O<T>;入参:Map.Entry<String, Object> 出参:Map。
getGenericInterfaces & getInterfaces
前者:获取由此对象表示的类或接口直接实现的接口的Type。
后者:获取由此对象表示的类或接口实现的接口
Eg:
class Food interface Eat<T> interface Run
class Dog implements Eat<Food>,Run
Type[] genericInterfaces = Dog.class.getGenericInterfaces();
//ParameterizedType Eat<Food>,Class Run
Class<?>[] interfaces = Dog.class.getInterfaces();
//Class Eat,Class Run
compareTo
比较两个对象
Eg:
a.compareTo(b) === a - b
红黑树结构
- 每个节点或为红,或为黑
- 根节点为黑色
- 每个叶子节点(NIL或NULL)为黑色
- 如果一个节点为红色,其子节点必须为黑色
- 从一个节点到任何子孙节点,路径上的黑节点相同
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ETsx3zMm-1654504639264)(:/96b08573bea242bb9ec7a9a2e094696a)]
面试题
为什么HashMap 的长度为什么是2的幂次方?
取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
HashMap里面的hash()返回值是key.hashCode() ^ (key.hashCode() >>> 16)
的返回值呢?
为了减少hash的冲突概率。
比如有两个key的hashCode()方法返回值分别如下
key1.hashCode(): 1111 1111 1111 1111 0101 0101 0111 0101
key2.hashCode(): 1111 1111 1110 1111 0101 0101 0111 0101
如果没有^ (h >>> 16)
,二者在底层的数组索引值是:15 & hash
:5
0000 0000 0000 0000 0000 0000 0000 1111
这是因为二者低16位完全相同,然后15 & hash
时,高位都是0,这就造成只有hash的低16位起了作用,而低16位完全相同,所以底层索引值也就相同了,这样很容易造成hash冲突。
但是如果有^ (h >>> 16)
就比如key.hashCode的值,也就是h变量如下所示:
1111 1111 1110 1111 0101 0101 0111 0101
高低16位参与运算,极大提高了hash的随机性,减少了hash冲突概率
Hashmap为什么引入链表?
hashmap的底层是数组,当map进行put()操作时候,会进行hash计算,判定这个对象属于数组的那个位置。当多个对象的值再同一个数组位置上面的时候,就会有hash冲突。这个时候就引入了链表
为什么jdk1.8会引入红黑树呢?
当链表长度大于8时,遍历查找效率较慢,故引入红黑树
并不是只需要链表长度大于8,同时需要满足条件数组长度大于64的时候变成红黑树。还有如果红黑树的节点个数小于6的时候,红黑树还会变成链表
HashMap为什么一开始不就使用红黑树?
因为红黑树相对于链表维护成本大,红黑树在插入新数据之后,可能会通过左旋、右旋、变色来保持平衡,造成维护成本过高,故链路较短时,不适合用红黑树。
HashMap的底层数组取值的时候,为什么不用取模
,而是&?
tab[i = (n - 1) & hash]
因为在计算机运算的时候,使用&比
取模
的性能更快。
数组的长度为什么是2的次幂?
- 为了让数据均匀分布,我们一般使用公式(hashCode % size)达到最大的平均分配。当容量为2的次幂,会满足一个公式:(n - 1) & hash = hash % n
- &运算速度快,比% / 等常规操作快了十倍有余
- 能保证 索引值 肯定在 capacity 中,不会超出数组长度
如果指定数组的长度不为 2次幂,就破坏了数组的长度是2次幂的这个规则吗?
不会的,HashMap 的 tableSizeFor 方法做了处理,能保证n永远都是2次幂。
多线程put并发的时候可能造成数据的丢失?
在
putVal
方法中,假设两个线程执行添加,并同时执行到#1位置,前者将被覆盖
多线程put和get并发的时候,可能造成get为null?
线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。
JDK7 中 HashMap 因为头插入,导致get时出现死循环?
/**
* Transfers all entries from current table to newTable.
*/
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);
e.next = newTable[i];
newTable[i] = e;
e = next;
} // while
}
}
- 有一数组容量为2,插入两个元素(a —> b)hash相同皆存入第一格
- 此时有两个线程A、B均执行put,存入第二格,扩容
- 两个线程分别创建新数组 A[],B[]
- 1.7 的 transfer 方法中有一关键代码:
Entry<K,V> next = e.next;
A线程执行到这里挂起,此时e为a,next为b - B线程执行结束,并且a、b依旧冲突,位于新数组的第一格,因为头插,此时顺序为B[(b -> a),null]
- A线程继续执行,将 e = a 置于新数组[a, null]
- 第二次 e = b,但由于B线程已经修改了原表,此时 b.next = a
- 于是A线程的新数组为:A[(a <—> b),null],
死循环
附录
结尾
HashMap
的方法不是线程安全的。并发put操作时发生扩容,可能会导致节点丢失,产生环形链表等。 节点丢失,会导致数据不准 生成环形链表,会导致get()方法死循环。
由于扩容时使用头插法,在并发时可能会形成环状列表,导致死循环,在jdk1.8
中改为尾插法,可以避免这种问题,但是依然避免不了节点丢失的问题。
HashMap
的设计初衷就不是在并发情况下使用,如果有并发的场景,推荐使用ConcurrentHashMap
如有任何疑问,参照附录;如有必要,参照源码