概览
- HashMap类使用散列表实现Map接口并扩展AbstractMap。HashMap采用哈希算法实现, 底层采用了哈希表存储数据,用 Node数组 实现
- HashMap 根据键的 hashCode 值计算出hash值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。
- HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。
- HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。
- 如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap
关键设计
概念
- size : HashMap中存放KV的数量(为链表和树中的KV的总和)
- capacity :指HashMap中桶node的数量。默认值为16。一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂。capacity 始终大于size
- loadFactor :装载因子,计算HashMap的实时装载因子的方法为:size/capacity
- threshold :hashmap所能容纳的最大数据量的Node个数,当HashMap的size大于threshold时会执行resize操作。 初始化时threshold=capacity*loadFactor
属性
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int modCount;
transient int size; // HashMap中存放KV的数量(为链表和树中的KV的总和)
final float loadFactor; // 装载因子(如果值太高, 虽说会提高空间利用率, 但是加大查找的开销)
int threshold; //下次扩容阀值(容量*加载因子) threshold=capacity*loadFactor
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量大小 16
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认填充比
static final int TREEIFY_THRESHOLD = 8; // 由链表转换成树的阈值
static final int UNTREEIFY_THRESHOLD = 6; // 由树转换成链表的阈值
// 当桶中的bin被树化时最小的hash表容量。
// 可树化的最小数组容量
//(如果表中的节点太多,则重新调整表大小)这个值至少是TREEIFY_THRESHOLD的4倍
static final int MIN_TREEIFY_CAPACITY = 64;
存储单元
node是hashmap的一个内部类,用来储存数据和保持链表结构的。它的本质就是一个映射(键值对)。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 重写hashCode
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// 重写 equals
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
设置容量大小
threshold = 返回 不小于给定目标容量cap的最小二次幂
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;
}
构造器
public HashMap() // 默认填充比为0.75
// threshold=(最小2次幂>=initialCapacity),默认填充比为0.75
// =HashMap(initialCapacity, DEFAULT_LOAD_FACTOR)
public HashMap(int initialCapacity)
public HashMap(int initialCapacity, float loadFactor) // 指定容量,指定填充比
{
if (initialCapacity < 0) //如果初始容量小于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)
hash
取key的hashCode值、高位运算、取模运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
jdk1.8确定map坐标的方式是tab[(n-1)&hash] n代表map的length,由于绝大多数情况下 map的length的值小于2^16 (25536),所以大部分情况下是hash的低16位与map的length进行与运算(&)
- 当length=8时 下标运算结果取决于哈希值的低三位
- 当length=16时 下标运算结果取决于哈希值的低四位
- 当length=32时 下标运算结果取决于哈希值的低五位
- 当length=2的N次方, 下标运算结果取决于哈希值的低N位。
resize扩容
- 原容量大于0,需要扩容,阈值扩容至原来的两倍,但最大值为Integer.MAX_VALUE
- 原数组=null,原阈值>0,初始容量设置为原阈值,新阈值设置为新容量0.75,即原阈值0.75(通常是指定容量的构造器 初始化扩容)
- 原数组=null,原阈值=0,采用默认值,容量=16,阈值=16*0.75(无参构造器初始化扩容)
- 保证新阈值为新容量*0.75
若原数组中存在值,需拷贝到新数组,遍历数组,数组位置上不为空,需转移,且把原引用置为null
- 原数组位置只有一个节点,根据hash计算下标存放到新数组
- 不止一个节点,且当前节点为树节点,按红黑树管理存入新数组
- 链表,按照新数组的下标计算方式存入新数组
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//oldTab变量指向原来的数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; //oldThr变量保存原来数组的临界值
int newCap, newThr = 0;
if (oldCap > 0) {//说明将要进行扩容操作
if (oldCap >= MAXIMUM_CAPACITY) {
//由于最大容量不能超过MAXMUM_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
// oldCap=0, 说明原来的table数组为null
// 初始容量设置为阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//oldCap=0, oldThr=0,所以一切参数采用默认值
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) {//如果原数组位置中的值!=null, 则需进行转移
oldTab[j] = null;//置为null, 方便进行GC
if (e.next == null)
//原数组中保存的hash值是没有冲突的, 也就是Node类型变量
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 原数组中保存的hash值存在冲突, 是红黑树 TreeNode 类型变量,
// 采用红黑树管理冲突的键值对
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 原数组中保存的hash值存在冲突,
//但是并没有用红黑树对冲突的Hash值进行管理, 而是用Node链表进行管理
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//因为需要根据冲突链表中的hash值存放到新数组中,而新数组的长度是原数组长度的2倍,
//newTable.length-1 比 oldTable.length-1 多oldCap, 因此 hash&(newTable.length-1)
//等价于 hash&(oldTable.length-1) + (hash&oldCap ==0 ? 0 : 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;
}
put
putValpublic V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
判断table是否为空,若空为其初始化扩容
- 根据 hash值 计算获得table下标 i= (table.length - 1) & hash 等价于对length取模,也就是hash %length(位运算只能用于除数是2的n次方的数的求余)
- 若table [i] == null ,填入添加的映射, size++,若size > threshold,扩容,return null;
若table [i] != null,判断当前位置的key是不是与将要存入的key相同,判断当前节点是否为树节点
- 判断当前位置,链头 的key 是否与将要存入的key相同,相同覆盖value,return原value
- key不同,判断p节点是否为树节点;若是,加入红黑树
key不同,不是树节点,则遍历链表,从头开始
- 若找到,代表是修改,覆盖原value,return原value
在链表中未找到与将要存入的key相同的key,代表是新增,若添加节点后,节点数>=8,将链表转化为红黑树,return原value
@param hash key的hash值
@param onlyIfAbsent if true, don't change existing value
@param evict if false, the table is in creation mode. 表处于创建模式
@return previous value, or null if none
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
// tab=初始map数组 ; n=初始map数组得长度
// p=hash计算后map数组 ; i=hash计算后map数组下标位置
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 当数组table为null时, 调用resize生成数组table, 并令tab指向数组table
n = (tab = resize()).length;
// 计算下标 将数据 赋值给 p (hash计算后map数组)
// 当前下标无数据 如果新存放的hash值没有冲突
if ((p = tab[i = (n - 1) & hash]) == null)
// 则只需要生成新的Node节点并存放到table数组中即可
tab[i] = newNode(hash, key, value, null);
else {
// 否则就是产生了hash冲突
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果hash值相等且key值相等, 则令e指向冲突的头节点
e = p;
else if (p instanceof TreeNode)
// 如果头节点的key值与新插入的key值不等,
// 并且头结点是TreeNode类型,说明该hash值冲突是采用红黑树进行处理.
// 向红黑树中插入新的Node节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 否则就是采用链表处理hash值冲突
// 遍历冲突链表, binCount记录hash值冲突链表中节点个数
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 当遍历到冲突链表的尾部时
// 生成新节点添加到链表末尾
p.next = newNode(hash, key, value, null);
// 如果binCount即冲突节点的个数>= (TREEIFY_THRESHOLD(=8) - 1),
//便将冲突链表改为红黑树结构,否则不需要改为红黑树结构
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在冲突链表中找到相同key值的节点, 则直接用新value覆盖原来value值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 说明原来已经存在相同key的键值对
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//onlyIfAbsent为true表示仅当<key,value>不存在时进行插入, 为false表示强制覆盖
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;// 修改次数自增
if (++size > threshold)
// 当键值对数量size达到临界值threhold后, 需要进行扩容操作
resize();
afterNodeInsertion(evict);
return null;
}
putTreeVal
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
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;
}
dir = tieBreakOrder(k, pk);
}
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;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
treeifyBin
//该方法的主要作用是将冲突链表改为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//当数组的长度< MIN_TREEIFY_CAPACITY(64) 时,只是单纯将数组扩容, 而没有直接将链表改为红黑树
//因为hash数组长度还太小时导致多冲突的主要原因, 增大hash数组长度可以改善冲突情况
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);
}
}
get
思路如下:
- bucket里的第一个节点,直接命中;
- 如果有冲突,则通过key.equals(k)去查找对应的entry
- 若为树,则在树中通过key.equals(k)查找,O(logn);
- 若为链表,则在链表中遍历通过key.equals(k)查找,O(n)。
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;
// first指向hash值对应数组位置中的Node节点
if ((tab = table) != null && (n = tab.length) > 0
&& (first = tab[(n - 1) & hash]) != null) {
// 如果first节点对应的hash和key的hash相等
//(在数组相同位置,只是说明 hash&(n-1) 操作结果相等, 说明hash值的部分低位相等,并不代表整个hash值相等),
// 并且first对应的key也相等的话, first节点就是要查找的
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) { // 说明存在hash冲突
if (first instanceof TreeNode) // 说明由红黑树对hash值冲突进行管理
return ((TreeNode<K, V>) first).getTreeNode(hash, key); // 查找红黑树
do { // 说明hash值冲突是由链表进行管理
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null); // 对链表进行遍历
}
}
return null;
}