面试问到了HashMap,然后在网上看了很多,发现看完还是不能完全理解,还是自己来写一篇加深了解。
首先现在基本都是基于JDK1.8开发了,所以我就不分成1.7和1.8了,直接就看1.8的源码,就像你现在去分析LOL S9的出装还有意义吗
1,HashMap继承于AbstractMap抽象类,实现了Map,Cloneable,Serializable接口
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
2,静态常量
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 //默认加载因子0.75,当数组元素个数/数组容量>这个值,会进行数组扩容
static final int TREEIFY_THRESHOLD = 8 //树化阈值默认8,当put新元素时,如果链表的个数大于这个值,会将链表转成树形结构
static final int UNTREEIFY_THRESHOLD = 6 //树退化阈值默认6,当resize扩容时,如果树的元素节点小于这个值,会把树转成链表存储
static final int MIN_TREEIFY_CAPACITY = 64 //最小树形化容量阈值默认64,hash表中的容量大于这个值才允许树化
3,四个构造方法
1)无参构造,只设置一个默认加载因子,不进行初始化
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2)指定容量的构造,这里调用了另一个构造方法,设置HashMap的初始容量和默认加载因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3)指定容量和加载因子的构造,设置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;
this.threshold = tableSizeFor(initialCapacity);
}
4)传入Map及其子类的构造
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
4,几个较为重要的方法
1)计算key的hash值
如果key是null,那么hash为0,所以hashmap中只能有一个key为null
如果key不为null,使用Object类的hashcode()方法计算得到一个int值hash,再将h右移16位与h进行异或操作,这叫做hash扰动算法,为什么要这样做呐,这就要说到另一个hash寻址算法了。
因为寻址算法是hash&数组长度 - 1,那么hash的高位无法参与运算,所以使用hash扰动算法让hash的高位也参与到运算中。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2)新增元素put操作
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put()调用了putVal()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
HashMap.Node<K,V>[] tab;
HashMap.Node<K,V> p;
int n, i;
//判断hashmap数组是不是null或者长度是否为0
if ((tab = table) == null || (n = tab.length) == 0)
//如果true,那么确认是第一次存值,就调用resize()进行初始化数组n=16,如果false,n=tab.length
n = (tab = resize()).length;
//数组长度n - 1和key的hash值进行与操作,得到一个 0 到 length-1 的一个值i,tab[i]得到具体某个节点p
if ((p = tab[i = (n - 1) & hash]) == null)
//如果这个节点为null,意思数组的这个节点为空,那么直接调用newNode将key和value存放进入
tab[i] = newNode(hash, key, value, null);
//如果这个节点不是null,那么意味着hash冲突了,需要进一步判断==或者equals方法
else {
HashMap.Node<K,V> e; K k;
//判断原节点p与存入key的hash值是否相等,以及==或equals方法的结果,如果为true,则返回原节点p的value结束put方法
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//上诉结果为false,意味着hash相同,==或equals不同,那么就要考虑遍历链表或者树挨个比较节点是否与新节点相同
//判断p是不是一个树节点
else if (p instanceof HashMap.TreeNode)
//如果p已经是一个树节点,那么调用putTreeVal()将新节点挂在树下面,注意在该方法内部也是拿出每个树节点挨个与新节点比较,看是否是相同节点
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果p不是树节点,就需要取出链表的每一个节点挨个与新节点比较
for (int binCount = 0; ; ++binCount) {
//如果链表的节点都遍历完了
if ((e = p.next) == null) {
//将新节点挂到链表上,注意这里是p.next,使用的尾插
p.next = newNode(hash, key, value, null);
//判断链表的长度是不是大于等于树化阈值8
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;
}
}
//当新节点要插入位置有数据时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;
//如果数组的使用量大于 数组大小*扩容阈值
if (++size > threshold)
//进行扩容操作
resize();
afterNodeInsertion(evict);
return null;
}
3)数组初始化及扩容操作resize()
final HashMap.Node<K,V>[] resize() {
//将数组赋值给oldTab
HashMap.Node<K,V>[] oldTab = table;
//判断数组是否为null,也就是数组是否初始化,没有初始化长度为0,初始化了将数组长度赋值给oldCap
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//将扩容临界点赋值给oldThr
int oldThr = threshold;
//定义新的数组长度和扩容临界点
int newCap, newThr = 0;
//如果数组长度oldCap大于0,意思是数组已经初始化过了
if (oldCap > 0) {
//如果数组长度大于等于最大数组长度
if (oldCap >= MAXIMUM_CAPACITY) {
//将int的最大值赋值给扩容临界点,实际上int的最大值比数组最大长度大,这样设置以后都不需要扩容了
threshold = Integer.MAX_VALUE;
//数组长度都大于等于数组最大长度了,那么就不需要用到resize进行扩容了,直接返回
return oldTab;
}
//如果旧的数组长度大于等于16并且旧数组长度*2以后小于最大数组长度
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容临界点*2
newThr = oldThr << 1; // double threshold
}
//扩容临界点大于0
else if (oldThr > 0) // initial capacity was placed in threshold
//将扩容临界点赋值给新的数组长度
newCap = oldThr;
//如果数组长度以及扩容临界点都没有定义
else { // zero initial threshold signifies using defaults
//使用默认数组长度进行初始化,默认长度16,使用默认扩容临界点12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果扩容临界点为0
if (newThr == 0) {
//给扩容临界点赋值,如果扩容临界点大于了数组最大值,就将int的最大值赋给它
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新的扩容临界点赋值给threshold
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建一个新的hash数组,长度为扩容后的长度
HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
//将扩容后的hash数组赋值给table
table = newTab;
//接下来是将旧数组的元素复制到新的数组中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
HashMap.Node<K,V> e;
//判断旧数组中指定位置是否为null,为空就进入下次循环,不为空将该元素赋值给e
if ((e = oldTab[j]) != null) {
//将旧数组该位置置空
oldTab[j] = null;
//判断取出元素的next是否为null,如果为null,说明没有链表和树结构
if (e.next == null)
//使用hash寻址算法,将e存入新数组的指定位置
newTab[e.hash & (newCap - 1)] = e;
//如果e是树状结构,意味着e下面挂着一棵树,需要一一遍历取出
else if (e instanceof HashMap.TreeNode)
//将树中的元素取出进行重新寻址存到新的数组中,如果树的元素不满6个,会转成链表形式存储
((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//这里表示e下面挂着一个链表
HashMap.Node<K,V> loHead = null, loTail = null;
HashMap.Node<K,V> hiHead = null, hiTail = null;
HashMap.Node<K,V> next;
do {
next = e.next;
/*
这个地方将每一个元素的hash取出来,与原数组长度进行按位与操作,有两种结果
1,结果为0
将该链表放到新数组在旧数组所在的索引位置
2,结果不为0
将该链表放到新数组在旧数组所在索引位置+旧数组长度的位置
这里很奇怪,旧数组只有单个元素时,会e.hash & newCap-1 进行寻址算法,放到新数组
为什么有链表的时候却不这样做
却按照e.hash & oldCap分两种情况存储到新数组
比如有一个hash是101011
101011 & 10000 == 0放到新数组在旧数组所在的索引位置,也就是11的位置
101011 & 11111 结果也是11,放到11的位置
另一个hash是1011011
1011011 & 10000 == 1放到新数组在旧数组所在索引位置+旧数组长度的位置,也就是11+16=27的位置
1011011 & 11111 结果也是27,放到27的位置
就这两种情况看来,其实这样做和重新做寻址算法是一样的效果。
因为oldCap-1=1111和2oldCap-1=11111就只有最高位不同,而oldCap=10000
那么只需要判断最高位与e.hash的结果即可,也就是e.hash & 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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//需要注意的是链表转红黑树的时候要判断数组的长度是否超过了64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
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);
}
}
hashmap是一个线程不安全的双列集合,内部使用数组,链表,红黑树存储。key和value可以为null,其中key依赖于hashCode()和equals()方法保证唯一性。
寻址算法的地方会导致并发覆盖问题,所以是线程不安全的。
if ((p = tab[i = (n - 1) & hash]) == null)
//如果这个节点为null,意思数组的这个节点为空,那么直接调用newNode将key和value存放进入
tab[i] = newNode(hash, key, value, null);
假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第13行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
put时先判断是不是第一次存值,是就初始化数组,然后使用hash扰动和寻址算法找到该元素应该存储的位置
1,如果该位置是null,直接存入
2,如果这个位置有元素,则判断新插入元素与旧元素equals和==
判断为true,返回原本数组中该位置的元素
判断为false,再看这个位置是链表还是红黑树
是红黑树就放到树中
是链表就尾插
transient Node<K,V>[] table;
文章详细介绍了HashMap在JDK1.8中的实现,包括其继承结构、静态常量、构造方法,特别是put操作的逻辑,涉及哈希计算、链表与红黑树的转换以及扩容机制。同时,指出了HashMap的线程不安全问题,举例说明了并发覆盖的情况。
43万+

被折叠的 条评论
为什么被折叠?



