HashMap使用HashMap(int initialCapacity)对集合进行初始化。
在默认的情况下,HashMap的容量是16。但是如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。比如如果指定了3,则容量是4;如果指定了7,则容量是8;如果指定了9,则容量是16。
现在来测试一下:
//测试容量
//设置初始化容量为9,结果自动计算 为16 。
Map<String , String> map3 = new HashMap<String, String>(9);
map3.put("A", "a");
Class<?> mapType = map3.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("实际容量:"+capacity.invoke(map3)); //16
当我们通过HashMap(int initialCapacity)设置初始容量的时候,HashMap并不一定会直接采用我们传入的数值,而是经过计算,得到一个新值,目的是提高hash的效率。比如1->1、3->4、7->8和9->16。
未初始化容量
int a= 10000000; //设置一个扩容的上限
Map<Integer,Integer> map = new HashMap<Integer, Integer>();
long s1 = System.currentTimeMillis(); // 获取一个时间节点
System.out.println(s1);
for(int i =0 ;i<a;i++) {
map.put(i, i);
}
long s2 = System.currentTimeMillis(); //获取第二个时间节点
System.out.println(s2);
System.out.println("消耗时间"+(s2-s1)); // 9430
初始化容量为5000000
Map<Integer,Integer> map1 = new HashMap<Integer, Integer>(a/2);
long s3 = System.currentTimeMillis();
for(int i=0;i<a;i++) {
map1.put(i, i);
}
long s4 = System.currentTimeMillis();
System.out.println("消耗时间:"+(s4-s3)); //6129
初始化容量为10000000
Map<Integer,Integer> map2 = new HashMap<Integer, Integer>(a);
long s5 =System.currentTimeMillis();
for(int i=0;i<a;i++) {
map2.put(i, i);
}
long s6 =System.currentTimeMillis();
System.out.println("消耗时间:"+(s6-s5)); //4470
以上的代码,我们创建了3个HashMap,分别使用默认的容量(16)、使用元素个数的一半(5百万)作为初始容量和使用元素个数(一千万)作为初始容量进行初始化,然后分别向其中put 一千万个KV。
从上面的打印结果中可以得到一个初步的结论:在已知HashMap中将要存放的KV个数的时候,设置一个合理的初始化容量可以有效地提高性能。下面我们来简单分析一下原因。
我们知道,HashMap是有扩容机制的。所谓的扩容机制,指的是当达到扩容条件的时候,HashMap就会自动进行扩容。而HashMap的扩容条件就是当HashMap中的元素个数(Size)超过临界值(Threshold)的情况下就会自动扩容。
threshold = loadFactor * capacity
在元素个数超过临界值的情况下,随着元素的不断增加,HashMap就会发生扩容,而HashMap中的扩容机制决定了每次扩容都需要重建hash表,这一操作需要消耗大量资源,是非常影响性能的。因此,如果我们没有设置初始的容量大小,HashMap就可能会不断发生扩容,也就使得程序的性能降低了。
另外,在上面的代码中我们会发现,同样是设置了初始化容量,设置的数值不同也会影响性能,那么当我们已知HashMap中即将存放的KV个数的时候,容量的设置就成了一个问题。
initialCapacity = expectedSize / 0.75F + 1.0F
initialCapacity = (需要存储的元素个数 / 负载因子) + 1
**-**这个算法实际上是JDK8中putAll()方法的实现。这是公式的得出是因为,当HashMap内部维护的哈希表的容量达到75%时(默认情况下),就会触发rehash(重建hash表)操作。而rehash的过程是比较耗费时间的。所以初始化容量要设置成expectedSize/0.75 + 1的话,可以有效地减少冲突,也可以减小误差。
总结
当我们想要在代码中创建一个HashMap的时候,如果我们已知这个Map中即将存放的元素个数,给HashMap设置初始容量可以在一定程度上提升效率。
但是,JDK并不会直接拿用户传进来的数字当做默认容量,而是会进行一番运算,最终得到一个2的幂。而为了最大程度地避免扩容带来的性能消耗,通常是把默认容量的数字设置成expectedSize / 0.75F + 1.0F。
这种算法实际上是一种使用内存换取性能的做法,在真正的应用场景中要考虑到内存的影响。