一、HashMap剖析
首先看看HashMap的顶部注释说了些什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
|
再来看看HashMap的类继承图:
下面我们来看一下HashMap的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
成员属性有这么几个
再来看一下,hashmap的一个内部类Node
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
我们知道Hash的底层是散列表,而在Java中散列表的实现是通过数组+链表的
再来简单看看put方法就可以印证我们的说法了:数组+链表=散列表
我们可以简单总结出HashMap
无序,允许为null,非同步
底层由散列表实现
初始容量和装在因子对HashMap影响挺大的,设置小了不好,设置大了也不好
1.1 HashMap构造方法
HashMap的构造方法有4个
1 2 3 4 5 6 7 8 9 10 11 12 |
|
判断初始化大小是否合理,如果超过就赋值最大值,初始化装载因子
在上面的构造方法的最后一行,我们会发现调用tableSizeFor(),我们进去看看
1 2 3 4 5 6 7 8 9 |
|
返回的是一个大于输入参数且最近的2的整数次幂的数
为什么是2的整数次幂呢?hash%length=hash&(length-1),但前提是length是2的n次方,并且采用&运算比%运算效率高
这里是一个初始化,在创建哈希表的时候,它会重新赋值(capacity*loadfactor)
其他的构造方法就不多说了。
1.2 put方法
put方法可以说是HashMap的核心,我们来看看
1 public V put(K key, V value) {
2 return putVal(hash(key), key, value, false, true);
3 }
调用了putVal方法,以key计算哈希值,传入key和value,还有两个参数
我们来看看是怎么计算哈希值的
1 static final int hash(Object key) {
2 int h;
3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
4 }
得到key的HashCode,与KeyHashCode的高16位做异或运算
为什么要这么干呢??我们一般来说直接将key作为哈希值不就好了,做异或运算是干嘛用的??
我们看下来
我们是根据key的哈希值来保存在散列表中的,我们表默认的初始容量是16,要放到散列表中也就是0-15的位置
也就是tab[n-1&hash],我们可以发现的是仅仅后四位有效,那如果我们key的哈希值高位变化很大,地位变化很小。直接拿过去&运算,这就会导致计算出来的Hash值相同的很多。
而设计者将key的哈希值的高位做了运算(与高16位做异或运算,使得在做&的时候,此时的低位实际上是高位和地位的结合),这就增加了随机性,减少了碰撞冲突的可能性。
下面我们再来看看流程是怎么样的
1.3 get方法
1 public V get(Object key) {
2 Node<K,V> e;
3 return (e = getNode(hash(key), key)) == null ? null : e.value;
4 }
计算key的哈希值,调用getNode获取相对应的value
接下来我们看看getNode()
是怎么实现的:
1 final Node<K,V> getNode(int hash, Object key) {
2 Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
3 if ((tab = table) != null && (n = tab.length) > 0 &&
4 (first = tab[(n - 1) & hash]) != null) {
5 if (first.hash == hash && // always check first node
6 ((k = first.key) == key || (key != null && key.equals(k))))
7 return first;
8 if ((e = first.next) != null) {
9 if (first instanceof TreeNode)
10 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
11 do {
12 if (e.hash == hash &&
13 ((k = e.key) == key || (key != null && key.equals(k))))
14 return e;
15 } while ((e = e.next) != null);
16 }
17 }
18 return null;
19 }
计算出来的哈希值是在哈希表上的,如果在桶的首位上就可以找到,那么就直接返回,否则就在红黑树或者链表中寻找。
1.4 remove方法
1 public V remove(Object key) {
2 Node<K,V> e;
3 return (e = removeNode(hash(key), key, null, false, true)) == null ?
4 null : e.value;
5 }
也是计算key的哈希值来计算来删除value
再来看看removeNode()
的实现:
二、HashMap和HashTable对比
从存储结构和实现来讲基本上都是相同的。它和HashMap的最大的不同是它是线程安全的,另外它不允许key和value为null。Hashtable是个过时的集合类,不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换
四、总结
在JDK8中HashMap的底层是数组+链表(散列表)+红黑树
在散列表中有装载因子这么一个属性,当装载因子*初始容量小于散列表元素时,该散列表会再扩散,扩容2倍
装载因子的默认值是0.75,无论初始大了还是初始小了,都对Hash Map的性能都不好
装载因子初始值大了,可以减少散列表的扩容次数,但同时会导致散列冲突的可能性变大(散列冲突也是耗性能的一个操作,要得操作链表红黑树)
装载因子初始值小了,可以减少冲突得可能性,但同时扩容得次数会变多
初始容量默认值是16,也一样,无论是大了还说小了,对我们得HashMap都是有影响的:
初始容量过大,那么我们遍历的速度就会受到影响
初始容量过小,那么再散列(扩容的次数)可能就变得多了,扩容也是一件非常耗性能的事情
从源码上我们可以发现:HashMap并不是直接拿Key的哈希值来用的,它会将key的哈希值的最高位16位进行异或操作,使得我们将元素放入哈希表的时候增加了一定的随机性。
还要值得注意的是:并不是桶子上有8位元素的时候它就能变成红黑树的,得同时满足散列表得容量大于64才行的