静态成员变量
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子(以前的版本也有叫加载因子的)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 这是一个阈值,当桶(bucket)上的链表数大于这个值时会转成红黑树,put方法的代码里有用到
static final int TREEIFY_THRESHOLD = 8;
// 也是阈值同上一个相反,当桶(bucket)上的链表数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中bin的数量大于了TREEIFY_THRESHOLD且桶的数量大于MIN_TREEIFY_CAPACITY,才会链表转红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
内部Node节点类
// 链表节点
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;
}
}
// 红黑树节点,LinkedHashMap.Entry<K,V> 继承 Node<K,V>
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
成员变量
// 存储元素的数组,长度总是2的幂次方
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
// 实际存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器 (修改次数)
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;
数据结构
数组
+ 链表
的结构实现的。每个数组单元中存放着一个单链表的头结点,其中每个结点是一个Node对象,通过Node对象封装Key-Value键值对,还有一个Node<K,V> next指向链表中下一个结点。HashMap采用拉链法解决哈希冲突,jdk1.8开始当桶里链表的元素数据>=8且桶的数量大64(MIN_TREEIFY_CAPACITY
)会将链表转成红黑树
寻址、碰撞检测、扩容机制
- 寻址
//先用hash()函数对key的hashcode进行高低半区异或运算,提高hashcode低半区的随机性
//再与数组长度-1进行位与运算得到下标
hash(key.hashCode()) & (table.length-1)
HashMap源码中模运算逻辑是把散列值和数组长度-1做一个位与
操作
这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为数组长度-1
相当于一个低位掩码
右边的值全是1。位与
操作的结果就是散列值的高位全部归零,只保留低位值.以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111
。和某散列值做位与
操作如下,结果就是截取了最低的四位值。h & (length - 1)
和 h % length
,它俩是等价不等效的,明显位运算
效率非常高。
01111010 00111100 00100101
& 00000000 00000000 00001111
----------------------------------
00000000 00000000 00000101 //高位全部归零,只保留末四位
- 碰撞检测
这得说说java对于对象的equals()与hashCode()的规定equals()相等的两个对象,hashcode()一定要确保相等
hashcode()相等的两个对象,equals()不一定相等
HashMap是根据key的hashcode确定散列表的下标位置的,如果HashMap中有两个key的hashcode相等或计算出来的下标相等,但equals不相等说明发生了碰撞
- 扩容机制
存入元素大于 > table.length* loadFactor
进入扩容,倍增扩容table.length << 1
hash算法
HashMap的hash函数作用就是提高hash值的随机性,从而降低碰撞几率,实现如下
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 右位移16位,正好是32bit的一半,高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来
// (h = key.hashCode()) ^ (h >>> 16) 效果如下
01111010 001111 00 00100101 11000101 // 原始哈希码 (key.hashCode())
^ 00000000 00000000 01111010 00111100 //原始哈希码的高半区(key.hashCode()>>>16)
-------------------------------------------------------------------
= 01111010 00111100 01011111 11111001 // 高半区信息完整保留,并加大低半区的随机性
- 设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,少了系统的开销,也不会造成因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞
为什么HashMap的初始容量是16?
HashMap的长度必需是2的n次方(2,4,8,16,32,64,....)或者1,这跟列表运算及扩容有关,默认16是基于性能跟空间的考量值
- hashmap通过key的
hashcode & (length-1)
得到散列表下标,因为2的n次方减1
的二进制表现为右边的值全是1,相当取模的效果,且比模运算性能好 - 扩容运算简单,只需要左移一位
16<<1 = 32
建议 初始容量(initialCapacity) = 需要存储的元素个数/负载因子(loadFactor) + 1, loadFactor默认0.75
实际上initialCapacity不管传什么值HashMap会采用第一个大于该数值的2的幂作为初始化容量,以下是HashMap的内部实现方法:
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;
}
图解HashMap实现原理
put操作
put完成后最终结果
get操作