HashMap简介
在jdk1.8中,HashMap采用的是数组+链表/红黑树的实现形式,其是线程不安全的。HashMap中可以存储键值为null的数值。HashMap有两个比较重要的参数,初始容量(initialCapacity)和负载因子(loadFactory),初始值分别为16和0.75。当HashMap中的元素个数达到当前容量乘以负载因子的乘积时,HashMap将会进行扩容,执行resize和rehash操作。
HashMap重要参数解析
1. 初始容量
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
初始容量:16,在注释中,jdk特别标注了初始容量必须是2的n次幂,这是什么原因呢?我们可以通过源码找找这个答案。
为什么数组大小一定要是2的幂
在put方法中,当我们调用put(key,value)方法,首先是计算key的hash值,然后通过hash值计算在数组中的下标位置,其计算下标的代码为 (n - 1)& hash。n在629行代码中将其赋值为了HashMap的数组大小,即其计算方式为HashMap的数组长度减1再跟哈希值进行按位与运算。
当数组长度是2的n次方,数组长度减1后它的二进制数就是都是1的值,此时对哈希值进行按位与操作得到的下标才是分布均匀的,如果不是2的n次幂,那么减1后二进制的值有的位就是0,此时该位与哈希值进行按位与操作就一直是0,就会导致有些哈希桶的值一直都是空的,值分布不均匀。
例如,假设我们的数组大小是16,减1后的值其二进制数低4位为 1111,高28位全是0。此时一个32位的哈希值与其进行按位与运算,假设该哈希值为0110110.......1101001,与1111进行按位与运算后得到的值为1001,那么1001即为数组下标的值。
当n = 2的幂时,(n - 1) & hash 等价于 hash % n。这里又有一个疑问了,既然是计算数组下标,为什么不直接用求余计算呢?仔细思考下可以发现,求余操作在这里有两个缺点:哈希值的范围是在 -2^31 ~ 2^31 -1这个范围内,它是有负数的,由于我们数组下标肯定都是要求正整数的,但是负数进行求余得到的还是负数,这是第一个缺点。第二个缺点是求余操作比起位运算来说还是较慢的。因此HashMap中计算下标采用了位运算。
2. 最大初始容量
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
最大初始容量不能大于2的30次方。因为int类型最大值是2^31 - 1,达不到2^31,所以最大初始容量只能取2^30。
3. 负载因子
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
负载因子的默认值为0.75,在源码中有这么一行注释解释了其为什么选择0.75的默认值:
大概意思是说默认负载因子(0.75)在时间和空间成本之间提供了一个很好的折衷方案。较高的值会减少空间开销,但会增加查找成本(在HashMap类的大多数操作中都得到了体现,包括get和put )。
4. 链表转成红黑树的阈值
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes