首先看下继承结构
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
继承自AbstractMap,并实现了Map等方法。
主要成员变量和一些常量
//默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//由链表转为红黑树的阈值=8
static final int TREEIFY_THRESHOLD = 8;
//由红黑树转化为链表的阈值=6
static final int UNTREEIFY_THRESHOLD = 6;
//最小的红黑树容量阈值=64,即容量大于等于64时才才会从链表变为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//节点数组
transient Node<K,V>[] table;
//实际元素个数
transient int size;
//负载因子
final float loadFactor;
//扩容阈值
int threshold;
构造方法
1.无参构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
如果不传负载因子,则将负载因子设为默认的,即0.75。
2.传入容量及负载因子
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);
}
根据传入的参数,生成相应的容量及负载因子。
tableSizeFor方法
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个方法比较巧妙,通过位运算,得到一个稍微大于或等于传入参数的2的n次方的数值,作为容量值返回。
例如如果你传入7,则自动会使容量为8。即稍大的2的N次方数据。
需要注意:
以上三种构造方法,生成map时,Node<K,V>[] table仍然为null。即数组并未在此时初始化,而是再之后添加真实数据时才真正生成该数组。
3.传入一个map作为参数的构造方法
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) {
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)
threshold = tableSizeFor(t);
}
else if (s > threshold)
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);
}
}
}
传入一个map,会对map遍历添加到当前map,涉及到扩容resize,和添加元素putVal,hash几个操作。
先看hash操作
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
此操作将元素key的hashcode进行扰动,即高16位与低16进行异或运算。使key能够更为平均地分配到table上。
添加元素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;
//如果table为空则先扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过与运算代替模运算,到的要存的数组下标值
if ((p = tab[i = (n - 1) & hash]) == null)
//如果该值为空,则存入当前值
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//判断该相同数据已存在,则不进行其他操作
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) {
p.next = newNode(hash, key, value, null);
如果链表node数量等于树化阈值,进行树化操作
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 说明map中已存在相同元素
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;
}
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
上面的代码获取数组下标的操作要着重说下:(n - 1) & hash
这个操作很巧妙,由于n是一个2的某次方,此时(n - 1) & hash 就等同于 hash % n 即取模运算。如10 模 8 就等于 10(1010) 与 7(0111) 都等于2 。显然位运算比模运算要高效的多。
预留的钩子操作,我们将会在后续文章中看到其用法。
扩容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;
//如果数组长度大于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
}
//如果数组为null,阈值>0,说明已经通过前三种构造函数生成map,将阈值设置为旧有阈值即可
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//如果数组为null,阈值为0,则将阈值和容量设置为默认
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;
//数组中的元素不为null
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//该元素的下一个元素为null,说明链表长度为1,即只有头节点
if (e.next == null)
//将该数据放到新数组中对应位置
newTab[e.hash & (newCap - 1)] = e;
//如果存在树结构的下一个节点,进行拆分树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//链表结构存在多个节点,将链表易一拆为二,分到新数组中
else { // preserve order
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);
//将地位链表放入新数组原先位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//将高位链表放入旧数组长度+j,即数组对应的高位
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
总的来说就是,将数组扩容一倍,阈值扩大一倍。并将旧数组或链表拆分到新的链表上。即实现扩容。
拆分规则如下(e.hash & oldCap) == 0则方法原先位置,否则放在oldCap+原位置。
这个操作也是利用了容量为2的N次幂的特性,用与运算代替取模运算。
上文看到旧有的位置计算使用hash与数组长度-1的与运算,代替hash对数组长度的取模运算。即位置=e.hash&(oldCap-1)=e.hash%oldCap。
那么扩容之后的位置=e.hash&(oldCap<<1-1),由于oldCap是2的某次幂,所以oldCap<<1-1就比oldCap-1多一位1。即如果oldCap=8,那么二进制oldCap<<1-1=1111,而oldCap-1=111。从而e.hash & oldCap即e.hash与oldCap与运算为0,也就是e.hash与oldCap<<1-1的最高位即1000与运算为0,结果自然等同于e.hash&(oldCap-1)。所以位置不变,放入低位。
那么如果e.hash & oldCap即e.hash与oldCap与运算为1,说明e.hash&(oldCap<<1-1)结果等于e.hash&(oldCap-1)加上一个最高位代表的数字,即oldCap,即原位置+旧数组长度。
从而实现既定的规则。
面试中经常问到的相关问题:
1.说一下HashMap的数据结构
Hashmap是用数组加链表或红黑树实现的,当链表长度大于8且数组长度大于等于64时,链表转化为红黑树。当红黑树的节点数量小于6时,转化为链表。
2.说一下put方法的完整流程
1.校验数组是否存在且长度大于0,否则扩容数组。
2.根据key的hashcode高低位扰动后的hash值,与数组长度-1做与运算,代替hash对数组长度的取模运算,得到放入数组的位置。
3.如果该位置不存在数据,将数据放入该点。
4.如果该位置存在数据,判断数据类型,如果是红黑树则将数据插入红黑树,否则插入链表尾部。如果该数据已存在,则不进行操作,直接返回该值。
5.判断容量是否超出阈值,如果超出进行扩容。
3.说一说hashmap的扩容机制
1.一般将数组扩容为之前的一倍。
2.将数组上的红黑树节点或链表节点,一分为二,放入新数组的高位链表和低位链表。
3.将两个链表根据计算规则,放入新数组的对应位置。计算方式是将hash值与旧数组长度进行与运算,得0放入低位,即原先位置。为1放入高位,即旧数组长度+原先位置。
4.为什么要扩容为2倍?
要遵守数组长度为2的幂数规则,该规则使用位与运算代替取模运算,更为高效地找到key所在的位置。
5.HashMap中的链表替换为数组可以吗?时间复杂度相同吗?
可以。时间复杂度不同。数组在增删时需要进行数据copy,时间负载度为O(n-i),n是数组长度,i是数据位置。而链表增删时间复杂度为O(1)。