集约型编程就是在考虑代码正确、保证效率的前提下,使用更少的内存,更少的cpu等资源。
问题引入:
我们来看下面一段代码,引入我们今天的话题:
@Test
public void generateParam() {
HashMap<String, String> paramMap = new HashMap<>(3);
paramMap.put("name", "xiaoMing");
paramMap.put("age", "18");
paramMap.put("address", "XX市XX路XX街道");
}
这是同事的一段类似代码,构建一个map类型的,长度为已知固定长度3个的方法入参。
从代码中可以看出,同事给hashMap的初始大小为3,问:在put 3个键值对的过程中,hashMap会扩容吗?
想一下,再看下面答案:
提示一下:HashMap的默认加载因子是0.75。
答案是不会。
原因是在初始化map的过程中,如果给定初始化长度,HashMap会初始化成向上取离它本身最近的2的指数幂。如:initialSize是3,向上取离它本身最近的2的指数幂是2^2,就是4。如果initialSize是5,2^2比5小,往上取,所以是2^3=8,初始化成长度为8的数组。
这个逻辑在jdk6和jdk8写法上有点差异,但结果是一样的。以jdk6为例(jdk6比较直观):
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity
// 这里的capacity就是从initialCapacity往上取最接近的2的指数幂
// threshold 为capacity * loadFactor
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
上面是初始化的过程,下面请看HashMap扩容的阀值:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//当size>=threshold时就进行扩容,size++是在比较完之后加的(不清楚的可以看看i++和++i的区别)
if (size++ >= threshold)
resize(2 * table.length);
}
所以我们来看一下一开始的例子代码的执行过程:
@Test
public void generateParam() {
HashMap<String, String> paramMap = new HashMap<>(3);
paramMap.put("name", "xiaoMing");
paramMap.put("age", "18");
paramMap.put("address", "XX市XX路XX街道");
}
首先initialCapacity=3,初始化的时候会自动向上取最近的2的指数幂,为4,而threshold 为capacity * loadFactor,当loadFactor使用默认值0.75时,threshold=3,所以当第三个元素put之后,在size++>=threshold表达式中,size为2(比较结束之后才变为3),而2>=3为false,所以不会扩容。
回头看来,3算是一个凑巧的数字了,在初始化时自动初始化成4了,你可以试试put7个参数,而创建HashMap的initialCapacity设为7,此时就会扩容了,因为7,向上取2指数幂是8,默认的threshold为6<7,所以会扩容了。
总结:
1、Map初始化如果要指定初始化大小,initialCapacity应为2的指数幂,避免自动调整。(就如上例中的3,初始化时会自动转为4)。
2、初始化时,在知道map的最终大小的前提下,要使得threshold>=n(n为固定的数据量 且threshold=capacity * loadFactor),而不是initialCapacity>=n
拓展阅读:
前面举的是jdk6的例子,下面我们来看一下jdk8的代码:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 计算threshold
this.threshold = tableSizeFor(initialCapacity);
}
下面来看一下tableSizeFor的算法:
/**
* Returns a power of two size for the given target capacity.
*/
// 从注释中我们就可以看出,下面的位运算就是向上取最近的2指数幂
static final int tableSizeFor(int cap) {
int n = cap - 1; // 解决刚好是2的指数幂的情况,如果刚好的2的指数幂,会使得无法与其他数统一处理
n |= n >>> 1; // 位的或运算,1|0=1,有一个1就等于1。 |= 或等,就与 n = n | n>>>1等价
n |= n >>> 2; // >>>三个大于号是逻辑右移,正数不影响,负数最高位补0,但试了一个,在这里逻辑右移>>>和算数右移>>对结果没影响
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个算法少一句都不行,设计得很巧妙,替换了jdk6中的while循环。我们来举个例子:
如果cap的大小为18,二进制为10010。
10010 | 01001 = 11011(n)
11011 | 00110 = 11111
11111 | 00001 = 11111
11111 | 00000 = 11111
11111 | 00000 = 11111
至此,我们发现该算法实现了整数级别(32位),将从第一个1开始,后面的0都变成1。
再看看这句:
int n = cap - 1; // 解决刚好是2的指数幂的情况,如果刚好的2的指数幂,会使得无法与其他数统一处理
tableSizeFor方法得到的不是最终的threshold,在put方法还会初始化,看下面代码的注释部分:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 关注resize方法
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 关注扩容的条件
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
查看resize()方法:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 前面算出来的threshold,就是2的指数幂
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
// 这里就是新的threshold,和jdk6的结果是一样的
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//以下略
我们可以看到,在put第一个元素的时候,进行了threshold的再一次初始化。
然后扩容的条件由:size++>=threshold 变成 ++size > threshold,这两句是等价的,是复习++i和i++的好例子。
所以,关于这个初始化大小的问题,jdk6和jdk8还是基本一样的。
有什么问题欢迎评论探讨!
码字不易,喜欢的话就点个赞吧。orz