HashMap 底层数据结构
数组 + 链表 + 红黑树
Hash算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
低16位与高16位进行异或运算,让其低16位同时保存了高16位的特点,避免Hash冲突
因寻址算法n - 1 的高16位一般都是0,也就是说实际地址只和key低16位有关,如果2个key低16位相同则容易产生Hash冲突,所以优化了高低16位异或运算
寻址算法
(n - 1) & hash(key)
数组长度n为2的n次方时,(n - 1) & hash(key) 相当于对数据取模运算,但是与运算效率更高
Hash冲突解决
链表 + 红黑树
链表节点源码
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
每一个节点都保存自身的hash,key,value和下一个节点
put值的时候,采用尾插法插入链表,链表达到一定长度进行树化,因为遍历链表时间复杂度是O(n) 遍历红黑树复杂度是log(n)
扩容优化
扩容为原来的2倍
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
扩容时机:当前Capacity > Capacity * loadFactor
扩容步骤:
新建一个Entry空数组,长度是原来的2倍
遍历原数组,进行ReHash计算位置
将hash(key) 与扩容后的 n-1 进行 & 运算,如果多出1个bit的1,则新位置为index + oldCap否则不变
Java 1.8的改变
我们知道,HashMap底层是使用数组存储Key-Value这样的实例,Java7叫做Entry,Java8叫做Node,是因为Java8要支持红黑树
put值的改变
Java7采用头插入法,作者可能考虑到后put入的值访问概率更大,优化链表查询效率
Java8之后都是尾插入法了
多线程环境下,Java7 resize采用头插法,同一位置上新的元素总是在链表的头部位置,也就是A --> B resize过后可能会出现 B --> A 这样就形成了死循环
而Java8采用尾插入方式,不会改变链表顺序,保持之前的引用关系
虽然Java8之后多线程下不会产生死循环,但是get/set方法没有加锁,也无法保证put进去的值get出来后还是原值,所以还是线程不安全的