HashMap源码详解(一)
一、HashMap中重要的成员变量
1.size
作用:记录Map中KV键值对的个数
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
2.loadFactor
作用:装载因子,用来衡量HashMap满的程度,默认值为0.75f
/**
* The load factor for the hash table.
*/
final float loadFactor;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
一般不建议修改loadFactor的值
3.capacity
作用:记录当前Map的容量
注:容量并未作为一个属性出现在源码之中,而是通过capacity()方法返回该Map的容量
transient Node<K,V>[] table;
final int capacity() {
return (table != null) ? table.length :
(threshold > 0) ? threshold :
DEFAULT_INITIAL_CAPACITY;
}
容量默认为16,位运算的速度最快,所以写成1<<4而不是16,且必须是2的整数次幂
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
最大容量为2的30次方,同样必须是2的整数次幂
static final int MAXIMUM_CAPACITY = 1 << 30;
用户也可以通过构造函数自定义初始化容量,如果自定义容量不是2的整数次幂,HashMap则会选择第一个大于该数字的2的幂作为容量。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);//此装载因子为默认数值
}
例如(以下代码通过反射调用capacity()方法):
public static void main(String[] args)throws Exception {
HashMap<String,String> map0 = new HashMap<>();
HashMap<String,String> map1 = new HashMap<>(3);
HashMap<String,String> map2 = new HashMap<>(5);
HashMap<String,String> map3 = new HashMap<>(9);
Class mc = map1.getClass();
Method method = mc.getDeclaredMethod("capacity");
method.setAccessible(true);
System.out.println("capacity: "+method.invoke(map0));//capacity: 16
System.out.println("capacity: "+method.invoke(map1));//capacity: 4
System.out.println("capacity: "+method.invoke(map2));//capacity: 8
System.out.println("capacity: "+method.invoke(map3));//capacity: 16
}
size和capacity的区别:
-
size为当前容器中已经存储的键值对个数
-
capacity为当前容器中可以存储的键值对个数
这里有一个小建议:在初始化HashMap的时候,应该尽量指定其大小。尤其是当你已知map中存放的元素个数时。(《阿里巴巴Java开发规约》)
4.threshole
作用:临界值,当实际KV键值对个数超过threshold时,HashMap将会扩大容量,threshold=容量*加载因子
/**
* The next size value at which to resize (capacity * load factor).
*/
int threshold;
loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,即3/4;而capacity又是2的幂。所以,两个数的乘积都是整数。对于一个默认的HashMap来说,默认情况下,当其size大于12(16*0.75)时就会触发扩容。
public static void main(String[] args)throws Exception {
HashMap<String,String> map = new HashMap<>();
Class mc = map.getClass();
Method method = mc.getDeclaredMethod("capacity");
method.setAccessible(true);
for (int i = 0; i < 13; i++) {
map.put(""+i,"hi");
if (i==11||i==12)
System.out.println("capacity: "+method.invoke(map));
}
}
扩容时:新容量大小=旧容量大小*2
二、HashMap容量的初始化
1.容量初始化的好处
在上文中我们提到过,如果在已知元素个数的前提下,在HashMap初始化时,最好指定其大小。
原因:在默认初始化容量和指定初始化容量(合理指定),后者性能更好。
原理:当HashMap容量大小不够时,会进行扩容操作。由于HashMap的本身特性,每次扩容时都需要重新建立Hash表,重建操作十分耗时。所以合理的指定初始容量可以减少扩容操作,提高性能。
证明:
public static void main(String[] args) {
int num = 10000000; //一千万
//未初始化容量
HashMap<String, String> map1 = new HashMap<>();
long start1 = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
map1.put(i+"", "hello");
}
long end1 = System.currentTimeMillis();
System.out.println("未初始化容量,耗时 : " + (end1 - start1)/60 + " s");
//初始化容量为元素数量一半
HashMap<String, String> map2 = new HashMap<>(num / 2);
long start2 = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
map2.put(i+"", "hello");
}
long end2 = System.currentTimeMillis();
System.out.println("初始化容量为元素数量一半,耗时 : " + (end2 - start2)/60 + " s");
//初始化数量为元素数量
HashMap<String, String> map3 = new HashMap<>(num);
long start3 = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
map3.put(i+"", "hello");
}
long end3 = System.currentTimeMillis();
System.out.println("初始化数量为元素数量,耗时 : " + (end3 - start3)/60 + " s");
}
//未初始化容量,耗时 : 144 s
//初始化容量为元素数量一半,耗时 : 98 s
//初始化数量为元素数量,耗时 : 55 s
2.容量初始化扩容的算法
//cap为自定义容量
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;
}
>>> : 无符号右移,忽略符号位,空位都以0补齐
上述代码主要分为两个部分:
第一部分:
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
理解:首先将原有数值-1,然后将该数值依次向右移位然后与原值取或。
原理:
整形为4个字节,则二进制位数最多有32位
原有数值-1后,从高位到低位,一定能找到第一个为1的数(先排除输入为0或1的情况)
设某一位为1,其它位为x(可能为0或1)
xxxx x1xx xxxx xxxx xxxx xxxx xxxx xxxx
n |= n >>> 1;
xxxx x1xx xxxx xxxx xxxx xxxx xxxx xxxx (原n)
0xxx xx1x xxxx xxxx xxxx xxxx xxxx xxxx (n>>>1)
xxxx x11x xxxx xxxx xxxx xxxx xxxx xxxx (新n)
通过该行代码,保证了第一个1以及其后1个位置一定为1,此时1的个数一定>=2个
n |= n >>> 2;
xxxx x11x xxxx xxxx xxxx xxxx xxxx xxxx (原n)
00xx xxx1 1xxx xxxx xxxx xxxx xxxx xxxx (n>>>1)
xxxx x111 1xxx xxxx xxxx xxxx xxxx xxxx (新n)
通过该行代码,保证了第一个1以及其后3个位置一定为1,此时1的个数一定>=4个
n |= n >>> 4;
xxxx x111 1xxx xxxx xxxx xxxx xxxx xxxx (原n)
0000 xxxx x111 1xxx xxxx xxxx xxxx xxxx (n>>>1)
xxxx x111 1111 1xxx xxxx xxxx xxxx xxxx (新n)
通过该行代码,保证了第一个1以及其后7个位置一定为1,此时1的个数一定>=8个
由上述规律可以看出,代码中移位操作的数值一定是已经确定的1的个数,每次经过移位或的操作之后,1的个数翻倍;
所以在最后一步移位16位并进行或操作之后,可以保证在这32位二进制数中,第一个为1的二进制位后所有的二进制数全为1;
最终结果:
xxxx x111 1111 1111 1111 1111 1111 1111
第二部分:
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
该部分比较简单,是一个临界值的判断
- 小于0(输入为0):结果为1
- 大于等于0(输入为1)且小于最大值:结果为n+1
- 大于最大值:结果为最大值
由于通过第一部分的转换,二进制数中1是连续的,所以在进行+1操作后,新的数值一定是2的整数次幂。
又因为在一开始,我们另输入数值减1,所以新的数值一定是第一个大于等于输入数值的2的整数次幂。
由此可见,一个很简单的容量扩容算法也包含了JAVA工程师很大的智慧。
3.合理的容量初始化数值
在知道了为什么要进行容量初始化和初始化扩容算法之后,一个新的问题出现了,什么样的数值是合理的初始化数值呢?
《阿里巴巴Java开发手册》有以下建议:
这个数值可以在我们需要存储的元素个数范围内减少扩容的次数,在性能上是一个良好的选择,尽管它需要牺牲一些内存。凡事都有两面性,由于在当今社会中,对于性能的提升已经成为了用户的主流需求,所在在时间和空间上,我们偏向于合理的牺牲空间换取时间。