蜗牛地梦想

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

一、HashMap底层实现原理(JDK1.8)
先来说说HashMap的几个特点,
1:是一个存储key-value数据对的集合。
2:key和value都可以存储null值。
3:key值不可以重复。
4:无序。
5:线程不安全。

二、HashMap底层数据结构,
HashMap底层是数组+链表+红黑树的实现,HashMap初始化时,是一个集合+链表的结合体,在数据结构中称为"散列链表",即:在集合中,每一个元素存储的是一个链表;JDK1.8之后,HashMap添加了一个属性TREEIFY_THRESHOLD,默认为值为8,当HashMap中put一个元素时,如果数组中该链表的长度达到8,则将链表转换为红黑树。

三、HashMap的属性解析

 1 HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{
 2     //table数组默认长度大小为16,必须为2的N次幂,减少hash碰撞
 3     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 4     //table数组的最大长度,2^30
 5     static final int MAXIMUM_CAPACITY = 1 << 30;
 6     //table扩容因子,table中的元素个数 > table.length * DEFAULT_LOAD_FACTOR时,table数组自动扩容为table.length的两倍
 7     static final float DEFAULT_LOAD_FACTOR = 0.75f;
 8     //由链表结构转换为红黑树的阈值,当链表中的数据大于8时,将链表结构转换为红黑树存储
 9     static final int TREEIFY_THRESHOLD = 8;
10     //由红黑树转换为链表结构的阈值,当红黑树的数量小于6时,将红黑树转换为链表结构存储
11     static final int UNTREEIFY_THRESHOLD = 6;
12     //由链表结构转换为红黑树时,还会有一次判断,只有键值对数量大于MIN_TREEIFY_CAPACITY时才会发生转换。
13     //这是为了避免哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的变化
14     static final int MIN_TREEIFY_CAPACITY = 64;
15 
16     //存放元素的数组
17     transient Node<K,V>[] table;
18     transient Set<Map.Entry<K,V>> entrySet;
19     //HashMap键值对的个数
20     transient int size;
21     //修改次数 fail-fast机制;如果遍历时,有其它修改了HashMap,那么将抛出ConcurrentModificationException
22     transient int modCount;
23     //扩容临界值 threshold = size * loadFactor ; 当HashMap中的数组元素个数 > threshold时,触发扩容操作
24     int threshold;
25     //装在因子,默认为0.75f 即:DEFAULT_LOAD_FACTOR
26     final float loadFactor;
27 }

四、HashMap的put(k , v)操作流程

 

 1 HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{
 2     public V put(K key, V value) {
 3         return putVal(hash(key), key, value, false, true);
 4     }
 5     //1:根据key值,计算hash值,如下:
 6     //计算HashMap中数组角标,高位16位 + 低位16位进行^操作,可以将key计算后的hash值更分散,减少hash冲撞
 7     static final int hash(Object key) {
 8         int h;
 9         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
10     }
11     //2:将key-value键值对放入指定的角标位置
12     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
13                    boolean evict) {
14         Node<K,V>[] tab; Node<K,V> p; int n, i;
15         //1:如果tab为null,或者talbe的长度=0;初始化table
16         if ((tab = table) == null || (n = tab.length) == 0){
17             n = (tab = resize()).length;
18         }
19         //(n-1) & hash 找到key在数组中存储的角标位置;如果为空,则直接新建链表节点Node
20         if ((p = tab[i = (n - 1) & hash]) == null){
21             tab[i] = newNode(hash, key, value, null);
22         }
23         //不为空
24         else {
25             Node<K,V> e; K k;
26             //头结点hash值与新put的key的hash值相同,且头结点的key值与新put的key值相同
27             if (p.hash == hash &&
28                 ((k = p.key) == key || (key != null && key.equals(k)))){
29                 e = p;
30             }
31             //如果头结点为红黑树节点TreeNode
32             else if (p instanceof TreeNode){
33                 // 红黑树的put方法比较复杂,putVal之后还要遍历整个树,必要的时候修改值来保证红黑树的特点
34                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
35             }
36             //头结点为链表节点Node
37             else {
38                 //遍历链表
39                 for (int binCount = 0; ; ++binCount) {
40                     //如果遍历到链表尾部,还没发现key值冲突的节点
41                     if ((e = p.next) == null) {
42                         //新建节点,并加到链表底部
43                         p.next = newNode(hash, key, value, null);
44                         //如果新增节点后,节点的数量达到阈值,则将链表转换为红黑树
45                         if (binCount >= TREEIFY_THRESHOLD - 1){ // -1 for 1st
46                             treeifyBin(tab, hash);//链表转换红黑树
47                         }
48                         break;
49                     }
50                     //如果key值一致,并且hash一致,替换
51                     if (e.hash == hash &&
52                         ((k = e.key) == key || (key != null && key.equals(k))))
53                         break;
54                     p = e;
55                 }
56             }
57             //将节点值更新为传递过来的value值,并返回key对应的旧值
58             if (e != null) { // existing mapping for key
59                 V oldValue = e.value;
60                 if (!onlyIfAbsent || oldValue == null)
61                     e.value = value;
62                 afterNodeAccess(e);
63                 return oldValue;
64             }
65         }
66         ++modCount;
67         //如果达到扩容的阈值,则进行扩容
68         if (++size > threshold){
69             resize();
70         }
71         afterNodeInsertion(evict);
72         return null;
73     }
74 }

 

五、HashMap的get(k)操作流程

 

 1 HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{
 2     public V get(Object key) {
 3         Node<K,V> e;
 4         //如果找不到对应的Node就返回null,如果找到就返回Node.value
 5         return (e = getNode(hash(key), key)) == null ? null : e.value;
 6     }
 7     //获取Node操作
 8     final Node<K,V> getNode(int hash, Object key) {
 9         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
10         //tab为不为空,并且长度大于0,并且角标[(tab.length-1) & hash]位置不为空
11         if ((tab = table) != null && (n = tab.length) > 0 &&
12             (first = tab[(n - 1) & hash]) != null) {
13             //第一个Node就是要找的节点,直接返回
14             if (first.hash == hash && // always check first node
15                 ((k = first.key) == key || (key != null && key.equals(k)))){
16                 return first;
17             }
18             if ((e = first.next) != null) {
19                 //如果是树,遍历红黑树复杂度是O(log(n)),得到节点值
20                 if (first instanceof TreeNode){
21                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
22                 }
23                 //如果是链表,遍历链表负责度是O(n),得到节点值
24                 do {
25                     if (e.hash == hash &&
26                         ((k = e.key) == key || (key != null && key.equals(k))))
27                         return e;
28                 } while ((e = e.next) != null);
29             }
30         }
31         return null;
32     }
33 }

 

六、总结

 

到这里,HashMap的原理已经基本说明白了,下面说一下我认为的比较重要的点:
1:HashMap的数据结构是数组+链表+红黑树,每次获取结果时,还需要遍历链表或者红黑树中的元素,因此,我们希望这个HashMap里面的元素位置分布均匀一些,如果每个数组每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,就可以马上知道赌赢位置的元素就是我们要的,而不需要再去遍历链表或者红黑树;此时,效率才是最高的。
那么,问题来了,获取每个key的hash值之后,如果才能将结果尽量的均匀分布在数组的索引上呢?
大家首先想到的应该是把hash值对数组长度做取模运算,这样一来,元素的分布是比较均匀的,但是,Java的取模运算的性能消耗还是比较大的,能不能找一种更快速,性能更高的方式呢?
HashMap中是这样做的:h & (table.length - 1);首先使用hash算法算得key的值(HashMap在这里将高16位和低16位进行了运算,是的分布更均匀),然后跟数组的长度-1做一次按位"&"运算(两个位的值同时为1,做"&"运算的结果采为1,其它结果都为0)。看上去很简单,其实不然,这里就需要说说为什么数组的长度必须为2^n(2的n次方)时,HashMap的分布才更均匀。
第一个数组长度为16,16-1=15 ,15二进制表示为1111;
第二个数组长度为15,15-1=14,14二进制表示为1110;
两组key的值分别为8和9,二进制标示分别为1000和1001;
1111 & 1000 = 1000 //数组长度16 15 & 8
1111 & 1001 = 1001 //数组长度16 15 & 9
1110 & 1000 = 1000 //数组长度16 14 & 8
1110 & 1001 = 1111 //数组长度16 14 & 9
我们发现,当数组长度为15时,hashcode的值会与14(1110)进行按位"&"操作,那么最后一位是0,而0001、0011、0101、1001、1011、0111、1101、1111这几个位置永远都不能存放数据了,空间浪费相当大,更糟的这几种情况是,数组中可以使用的位置比数组长度晓蕾很多,这意味着进一步增加了hash碰撞的几率,减慢了查询的效率。
所以,当数组长度为2^n次幂时,不同的key算的的index相同的几率较小,那么数据在数组中分布就比较均匀,也就是说碰撞的几率较小,相对的查询的时候遍历的次数就少,这样查询效率也就高了。
2:HashMap的resize()方法
当HashMap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就需要对HashMap的数组进行扩容,数组扩容的这个操作也会出现在ArrayList中,所以这是一个通用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap在什么时候进行扩容呢?当HashMap中的元素个数超过length*loadFactor时,就会进行扩容。loadFactor的值默认为0.75,也就是说默认情况下数组大小为16,那么当HashMap数组中的元素个数超过16*0.75=12的时候,就需要将数组的大小扩展为2*16=32,然后计算每个元素在数组中的位置,而这时一个非常消耗性能的操作。
所以,如果我们已经预知HashMap中元素的个数,那么预设HashMap元素的个数能够有效提高HashMap的性能。例如:如果元素预计为1000,我们必须new HashMap(2048)才最合适。

 

posted on 2017-03-18 05:17  蜗牛地梦想  阅读(114)  评论(0)    收藏  举报