hash算法
hash算法分为两部分,计算key的hash值,通过key的hash值来定位数据所在的位置。首先是计算key的hash值的方法。hash方法通过将key的hashCode的高16与低16位进行按位异或计算。目的是将高位的特征与低位的特征相结合,获取分布更加均匀的hash值,减少hash冲突的概率。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
通过下面的公式来定位数据的位置。n为hashMap桶位的容量,计算位置的方法是hash%n,但是这里不是用的取模运算,而是用按位与运算替代。这两个公式相等需要有一个前提条件就是n为2的x次方。hash%n计算为hash除以n的余数,当n为2的x次方时,hash/n 等价于 hash >> x,那么右移出的x位则为hash/n的余数。得到此数据的一个方法是拿x位1与hash做按位与运算就可以计算出hash的右边x位。由于n=2x,所有n-1就为x位1。由此可得当n=2x时,(n-1)&hash = hash%n。
(n - 1) & hash
初始化容量
默认初始化容量为16,如果创建时传递了initialCapacity参数,通过tableSizeFor计算大于initialCapacity的最小的2的N次方。
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;
}
int类型二进制为32位,这里通过移位操作来将n的全部位转为1,然后通过n+1就得到了大于n的最小的2的N次方。先减1的目的是为了处理cap本身就为2的N次方的情况。
负载因子与扩容阀值
负载因子默认为0.75,扩容阀值为当前容量*负载因子。当达到扩容阀值时,通过resize方法将容量扩充为当前容量的2倍。比如容量为16当存储的数据数量超过12时,会自动扩容为32。
hash冲突时的处理
HashMap中处理hash冲突的方式为拉链法,当有冲突发生时,会将hash值相同的数据以一个单链表的形式存储在同一个位置。在1.8及以后对此中情况进行了优化,在链表的长度达到8时会将链表转为红黑树以提高查询数据的性能。
put方法
- 是否已经初始化数组,如果没有初始化,先通过resize方法进行初始化。
- 通过key的hash值计算对应位置,判读是否有元素存在,如果没有直接存入。
- 如果存在元素,判断是链表还是红黑树然后存入数据
- 判断是否达到了扩容阀值,如果达到了,进行扩容
get方法
- 通过key的hash值计算应在位置
- 不存在数据返回null
- 存在数据,判断是链表还是红黑树
- 遍历链表或者红黑树,比较key是否相同,相同返回数据,没有相同的返回null
遍历方式
- entrySet
- keySet
- values
这三种方式全部是通过内部类以HashMap数据的视图方式实现,直接使用HashMap中的存储结构,不需要额外的存储空间。最推荐的方式是entrySet方式遍历数据,因为跟HashMap存储数据的格式一致,直接获取key-value键值对,keySet需要先获取key然后再通过get方法来获取value。
并发情况下有什么问题
当多个线程同时操作一个HashMap时,碰到同时修改HashMap结构的操作时(添加或者删除键值对),会出现数据丢失或者死循环。