HashMap底层是通过动态数组+链表(或红黑树),具有以下特点:
- 数组的动态扩容保证
- 链表与红黑树的转化
- 每一个存储的K-V对象都是一个Map.Entry<K,V>对象
红黑树
红黑树是一种特殊的平衡二叉树(AVL)。红黑树在插入和删除上比平衡二叉树效率高;在数据的查询上,由于可能存在的树的高度比AVL树高一层,查询性能略差。红黑树具有以下特点:
- 每一个节点都有一个标志位标识,或者是黑色,或者是红色
- 根节点一定是黑色
- 每个叶子节点是黑色
- 如果一个节点是黑色,它的子节点一定是红色
- 从一个节点到该节点的子孙节点的所有路径上,包含相同数目的黑色节点
HashMap的重要属性
// 初始默认容量,必须是2的常数幂
static final int DEFAULT_INITIAL_CAPACITY = 1<<<4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 加载因子
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;
// 元素存储数组,第一次使用时初始化
transient Node<K,V>[] table;
// 键集合
transient Set<Map.Entry<K,V>> entrySet;
// map中元素个数
transient int size;
// 调整容量的临界值 (capacity * load factor)
int threshold;
// 加载因子,创建时不传入,默认DEFAULT_LOADER_FACTOR
final float loadFactor;
// 静态内部类,单项链表
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
// 对象哈希值,键、值的哈希值进行异或运算
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// 替换Value,返回Old
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
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;
}
}
HashMap 中下标计算
(1)计算哈希值,key的哈希值与高低16位进行异或运算
// 将Key的哈希值进行高低16位的异或运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么需要进行高低16位的异或运算?
HashMap在进行下标计算时,采用公式:hash&(length-1),即上述方法计算的哈希值hash与length-1进行"与运算"。
通常情况下,我们在使用Map时,绝大多数情况下,map中的数据不是很多,一般小于2^16即65536.所以h&(length-1)始终是h的低16位参与运算,高16位始终处于浪费状态。而且当key.hashCode()得到的数值低16位相同,高16位差异时,更容易出现哈希冲突。例如:
(1) key.hashCode()直接与(length-1)进行& 运算
length=16,(length-1)=15,转化为二进制: 0000 1111
key.hashCode()的数值为236,829,409,转化为二进制:0000 1110 0001 1100 1011 1010 1110 0001
进行&运算
0000 1110 0001 1101 1011 1010 1110 0001
&
0000 0000 0000 0000 0000 0000 0000 1111
……………………………………………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 0001
计算之后,获得下标为1
假如key.hashCode() = 236,829,425
0000 1110 0001 1101 1011 1010 1111 0001
&
0000 0000 0000 0000 0000 0000 0000 1111
……………………………………………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 0001
下标仍然是1
(2)key.hashCode()先进行高低位异或运算,再进行&运算
key.hashCode()=236,829,409,高低16位进行异或运算
0000 1110 0001 1101 1011 1010 1110 0001
^
0000 0000 0000 0000 0000 1110 0001 1101
……………………………………………………………………………………………………………………
0000 1110 0001 1100 1011 0100 1111 1101
&
0000 0000 0000 0000 0000 0000 0000 1111
……………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 1101
下标位:13
假如key.hashCode() = 236,829,425
0000 1110 0001 1101 1011 1010 1111 0001
^
0000 0000 0000 0000 0000 1110 0001 1101
…………………………………………………………………………………………………………………………
0000 1110 0001 1100 1011 0100 1110 1100
&
0000 0000 0000 0000 0000 0000 0000 1111
……………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 1100
下标位:12
当key.hashCode()先进行高低16位异或运算之后,可以让高16位参与后续的与运算,可以减少哈希冲突,下标更加散列。
为什么用^而不是用&或者|运算?
因为 & 或者 | 运算,均偏向于0或者1,并不是均匀的概念,而^是去除相同取差异,会更加随机
&运算:0:75% 1:25%
|运算:0:25% 1:75%
^运算:0:50% 1:50%
补充
- 当length = 8时,下标计算结果取决于哈希值的低3位
- 当length = 16时,下标计算结果取决于哈希值的低4位
- 当length = 32时,下标计算结果取决于哈希值的低5位
- 当length = 2^n时,下标计算结果取决于哈希值的低n位
源码分析之构造函数
// 1、无参构造函数,构建一个空的HashMap,默认初始容量为16,加载因子为0.75f。
// 此时变量table并未实例化
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 2、构建一个空的HashMap,指定初始容量,加载因子默认为0.75f
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 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
// 长度除以负载因子,可能会出现小数,+1 是为了下一步向上取整
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);
}
}
}
// 4、创建一个指定初始容量和加载因子的空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);
}
// 返回不小于给定数据且距离最近的2的N次幂
static final int tableSizeFor(int cap) {
// 为了排除本身为2的N次幂,例如 cap = 8,若不进行减一,计算后获得的是16
int n = cap - 1;
// 以下做法是为了将最高位1之后的所有数字变为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;
}
tableSizeFor 算法举例
例如cap = 9,则n =8,转为二进制:0000 0000 0000 0000 0000 0000 0000 1000
n >>> 1: 0000 0000 0000 0000 0000 0000 0000 0100
n |= n >>> 1:
0000 0000 0000 0000 0000 0000 0000 1000
|
0000 0000 0000 0000 0000 0000 0000 0100
……………………………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 1100
n|=n >>> 2:
0000 0000 0000 0000 0000 0000 0000 1100
|
0000 0000 0000 0000 0000 0000 0000 0011
……………………………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 1111
之后的右移4位、8位、16位,均为0.
思考:为什么必须是2的N次幂?
源码分析之PUT
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 通过高低16位异或运算,更加随机、散列
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* Implements Map.put and related methods
* @param hash key.hashCode()高低16位异或运算之后 的哈希值
* @param key the key
* @param value the value to put
* @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) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 当table数组为为空时,初始化table。即第一次添加数据时,对HashMap进行初始化
if ((tab = table) == null || (n = tab.length) == 0){
n = (tab = resize()).length;
}
// i =(n-1) & hash,计算下标位。即当table数组中,i下标数据为null时,直接创建节点
// 若不为null,则确定索引位置,则进行赋值
if ((p = tab[i = (n - 1) & hash]) == null){
tab[i] = newNode(hash, key, value, null);
} else {
Node<K,V> e;
K k;
// 若hash、key、以及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);
}
// 否则,确定了索引位置,但是链表的第一个节点,不能与key进行匹配,需要对链表进行循环
else {
// 循环链表
for (int binCount = 0; ; ++binCount) {
// 若节点的下一个节点为null,则直接创建新的节点,放在该节点之后,即next
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 判断节点数量是否大于“树化”临界值,若大于临界值,需要将链表转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1){ // -1 for 1st
treeifyBin(tab, hash);
}
break;
}
// 若完全匹配,则跳出循环,说明key对应的节点已经存在,则返回节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
break;
}
p = e;
}
}
// 节点存在,根据条件,对value进行赋值
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 将原值进行覆盖
if (!onlyIfAbsent || oldValue == null){
e.value = value;
}
// 空函数,LinkedHashMap使用
afterNodeAccess(e);
return oldValue;
}
}
// 赋值操作后,集合操作数+1
++modCount;
// 当前集合元素加1,并判断是否需要扩容
if (++size > threshold){
resize();
}
// 空函数,LinkedHashMap实现
afterNodeInsertion(evict);
return null;
}
// 初始化数组及后续扩容
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
}
// 原table数组为空,但阈值不为空时,对newCap进行赋值
// 场景:HashMap的构造方式有4种,当采用第2、3种方式构建时,传入了initialCapacity,
//这时table并没有初始化,但经过tableSizeFor(initialCapacity)方法,threshold已经被赋值
}else if (oldThr > 0){
newCap = oldThr;
// 默认初始化
}else {
// newCap = 16
newCap = DEFAULT_INITIAL_CAPACITY;
// newThr = 16*0.75 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 第2、3种构造方法创建的HashMap
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;
// 原数组不为空,需要将元素转移,否则直接返回newTab
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 判断节点是否为空,并赋值给e
if ((e = oldTab[j]) != null) {
// 将原数组节点置为null
oldTab[j] = null;
// 如果该下标存储的只有一个节点,而不是链表,则直接转移,
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;
// 判断扩容后,高位是否为0,如果为0,则下标不变,若不为0,下标=原下标+原数组长度
// 不需要再次计算hash
// 这也是每次扩容,均为2的N次幂的原因
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;
}
下面以oldCap=16进行举例,则newCap = 32.下标计算为 hash&(length-1)
hash&length ==0时,扩容后,元素的下标不变;
length=16,hash的数值为236,829,411,转为二进制:0000 1110 0001 1101 1011 1010 1111 0001
计算下标:
0000 1110 0001 1101 1011 1010 1110 0011
&
0000 0000 0000 0000 0000 0000 0000 1111
………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 0011 ->下标位置为3
hash & length:
0000 1110 0001 1101 1011 1010 1110 0011
&
0000 0000 0000 0000 0000 0000 0001 0000
…………………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 0000 ->0,下标位置不变
验证:当扩容至32时,进行下标计算:
0000 1110 0001 1101 1011 1010 1110 0011
&
0000 0000 0000 0000 0000 0000 0001 1111
………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 0011 ->下标为3
hash&length !=0时,扩容后,元素的下标new = old + lenght;
length=16,hash的数值为236,829,425,转为二进制:0000 1110 0001 1101 1011 1010 1111 0001
计算下标:
0000 1110 0001 1101 1011 1010 1111 0001
&
0000 0000 0000 0000 0000 0000 0000 1111
………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 0001 ->下标位置为1
hash & length:
0000 1110 0001 1101 1011 1010 1111 0001
&
0000 0000 0000 0000 0000 0000 0001 0000
…………………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0001 0000 ->不为0,则扩容至32时,下标为1+16 = 17.
验证:当扩容至32时,进行下标计算:
0000 1110 0001 1101 1011 1010 1111 0001
&
0000 0000 0000 0000 0000 0000 0001 1111
………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0001 0001 ->下标为17
PUT方法逻辑图:
回顾之前问题,为什么长度是2的N次幂?
-
计算出来的下标更加散列。下标计算i时,先计算hash=key.hashCode()^(key.hashCode()>>>16),
i = hash&(length-1),table长度是2的N次幂,可使length-1低位均为1,排除长度的过分干扰。
-
扩容后,数据的移位更加简单有效。由于长度是2的N次幂。数组扩容后,节点是链表或者红黑树的前提下,只需要考虑hash&length是否为0,若为零,则下标不变;若不为零,则new = old +oldLength
源码分析之GET
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 通过hash&(length-1)定位下标,若不为Null,进行key判断
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n; K k;
// 如果table不为Null、table中含有元素、且计算出的下标不为null,
// 否则直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果第一个节点的key相同,直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k)))){
return first;
}
// 如果不同,则进行循环判断
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;
}
GET之逻辑图
注:以上仅为个人学习记录,错误之处,请指正。
另:若有侵权,请告知。