前言
注意:我们今天所有的一切都是基于 JDK 8,JDK 8 的实现和 JDK 7 有重大区别。
前面我们分析了 hashCode 和 hash 算法的原理,其实都是为我们解析 HashMap 做铺垫,因为 HashMap 确实比较复杂(如果你每一行代码都看的话,每个位移都纠结的话),虽然总的来说,HashMap 不过是 Node 数组加 链表和红黑树。但是里面的细节确是无比的优雅和有趣。楼主为什么选择 put 方法来讲呢?因为从楼主看来,HashMap 的精髓就在 put 方法中。
HashMap 的解析从楼主来看,主要可以分为几个部分:
1. hash 算法(这个我们之前说过了,今天就不再赘述了)
2. 初始化数组。
3. 通过 hash 计算下标并检查 hash 是否冲突,也就是对应的下标是否已存在元素。
4. 通过判断是否含有元素,决定是否创建还是追加链表或树。
5. 判断已有元素的类型,决定是追加树还是追加链表。
6. 判断是否超过阀值,如果超过,则重新散列数组。
7. Java 8 重新散列时是如何优化的。
开始吧!!!
1. 初始化数组
首先我们来一个测试例子:
public static void main(String[] args) {
HashMap<String, Integer> hashMap = new HashMap<>(2);
hashMap.put("one", 1);
Integer one = hashMap.get("one");
System.out.println(one);
}
}
一个简单的不能再简单的使用 HashMap 的例子,其中包含了对于 HashMap 来说关键的 3 个步骤,初始化,put 元素,get 元素。
由于我们预计会放入一个元素,出于性能考虑,我们将容量设置为 2,既保证了性能,也节约了空间(置于为什么,我们在之前的文章中讲过)。
那么我们就看看 new 操作的时候做了些什么事情:
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//=================================================================================
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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;
this.threshold = tableSizeFor(initialCapacity);
}
上面是 HashMap 的两个构造方法,其中,我们设置了初始容量为 2, 而默认的加载因子我们之前说过:0.75,当然也可以自己设置,但 0.75 是最均衡的设置,没有特殊要求不要修改该值,加载因子过小,理论上能减少 hash 冲突,加载因子过大可以节约空间,减少 HashMap 中最耗性能的操作:reHash。
从代码中我可以看到,如果我们设置的初始化容量小于0,将会抛出异常,如果加载因子小于0也会抛出异常。同时,如果初始容量大于最大容量,则重新设置为最大容量。
我们开最后两行代码,首先,对负载因子进行赋值,这个没什么可说的。
牛逼的是下面一行代码:this.threshold = tableSizeFor(initialCapacity); 可以看的出来这个动作是计算阀值,上面是阀值呢?阀值就是,如果容器中的元素大于阀值了,就需要进行扩容,那么这里的这行代码,就是根据初始容量进行阀值的计算。
我们进入到该方法查看:
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
一通或运算和无符号右移运算,那么这个运算的的最后结果是什么呢?这里其实就是如果用户输入的值不是2的幂次方(我们通过之前的分析,应该直到初始容量如果不是2的幂次方会有多么不好的结果)。通过位移运算和或运算,最后得到一定是2的幂次方,并且是那个离那个数最近的数字,我们仔细看看该方法:
| : 或运算,第一个操作数的的第n位于第二个操作数的第n位 只要有一个是1,那么结果的第n为也为1,否则为0
首先,将容量减1,我们后面就知道了。然后将该数字无符号右移1,2,4,8,16,总共移了32位,刚好是一个int类型的长度。在移动的过程中,还对该数字进行或运算,为了方便查看,楼主写一下2进制的运算过程,假如我输入的是10,明显不是2的幂次方。我们看看会怎么样:
10 = 1010;
n = 9;
1001 == 9;
1001 >>> 1 = 0100;
1001 或 0100 = 1101;
1101 >>> 2 = 0011;
110 或 0011 = 1111;
1111 >>> 4 = 0000;
1111 或 0000 = 1111;
1111 >>> 8 = 0000;
1111 或 0000 = 1111;
1111 >>> 16 = 0000;
1111 或 0000 = 1111;
最后,1111 也就是 15 ,15 + 1 = 16,刚好就是距离10 最近的并且没有变小的2的幂次方数。可以说这个算法非常的牛逼。楼主五体投地。
但是如果是 16 呢,并且没有不减一,我们看看什么结果:
16 = 10000;
10000 >>> 1 = 01000;
10000 或 01000 = 11000;
11000 >>> 2 = 00110;
11000 或 00110 = 11110;
11110 >>> 4 = 00001;
11110 或 00001 = 11111;
11111 >>> 8 = 00000;
11111 或 00000 = 11111;
1111