java容器三:HashMap源码解析

时间:2021-09-22 15:00:24

前言:Map接口

map是一个存储键值对的集合,实现了Map接口的主要类有以下几种

java容器三:HashMap源码解析

TreeMap:用红黑树实现

HashMap:数组和链表实现

HashTable:与HashMap类似,但是线程安全

LinkedHashMap:与HashMap类似,但是内部有一个双向链表来维持插入顺序或其他某种要求的顺序

下面来介绍HashMap的具体信息

 

存储结构

链表

1 static class Node<K,V> implements Map.Entry<K,V>{
2  final int hash;
3  final K key;
4  V value;
5  Node<K,V>next;
6 }

Node<K,V>是用来存储一个键值对的,从next上我们可以看出这是一个链表结构,每一条链表上存储的是hash值相同的结点也就是键值对

 

哈希桶

 1 transient Node<K,V>[] table; 

哈希桶是一个数组结构,数组的每一个元素保存一条链表

 

所以HashMap内部是采用“拉链法”来实现,示意图如下

java容器三:HashMap源码解析

 

 

确定桶下标

确定桶下标也就是确定键值对保存在数组中的下标,是根据key的哈希值来确定的,是用key的哈希值取模桶的长度得到。

虽然key的hashCode是int型取值有40多亿,但是由于桶长度远远小于hashCode能够取到的值,就会常常发生取模后得到的下标值相同的情景,

这称为“哈希碰撞”。为了解决这个问题,java设置了扰动函数来尽量减少哈希碰撞,就是代码中的hash()函数

1     /**找到存放该key的桶的下标时,是通过hashCode与桶的长度取余得到的。        
2      *由于取余是通过与操作完成的,会忽略hash值的高位。
3      * 扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,
4      * 相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)
5      */
6     static final int hash(Object key) {
7         int h;
8         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
9     }    

 

扩容方法

介绍

设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。

为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。

和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。

变量 含义
capicity 哈希桶的容量,默认为16,注意capicity一定是2的N次方(因为hashmap中很多运算都是用位运算代替的,2的N次方才会使位运算满足代码逻辑)
size 键值对的数量
threshold 阀值。当键值对的数量size>threshold的时候就会发生扩容
load_factor 装载因子。threshold = capicity*load_factor

 

扩容方法源码解析

java容器三:HashMap源码解析java容器三:HashMap源码解析
  1     final Node<K,V>[] resize() {
  2         //初始哈希桶
  3         Node<K,V>[] oldTab = table;
  4         //初始哈希桶容量
  5         int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6         //初始阀值
  7         int oldThr = threshold;
  8         //设置新的哈希桶容量和阀值都为0
  9         int newCap, newThr = 0;
 10         //当前容量>0
 11         if (oldCap > 0) {
 12             //如果旧哈希桶容量超过最大值,将阀值设为int的最大值1<<31-1,不扩容直接返回旧哈希桶
 13             if (oldCap >= MAXIMUM_CAPACITY) {
 14                 threshold = Integer.MAX_VALUE;
 15                 return oldTab;
 16             }//否则新的容量为旧的容量的2倍
 17             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 18                      oldCap >= DEFAULT_INITIAL_CAPACITY)
 19                 //阀值也为旧阀值的2倍
 20                 newThr = oldThr << 1; // double threshold
 21         }//当前哈希桶是空的,但是有阀值的,代表的是初始设置了容量和阀值的情况
 22         else if (oldThr > 0) // initial capacity was placed in threshold
 23             newCap = oldThr;
 24         else {   //当前哈希表是空的且没有阀值,代表的是无参构造器的情况,则需要进行初始化
 25             // zero initial threshold signifies using defaults
 26             //容量设置为默认容量16
 27             newCap = DEFAULT_INITIAL_CAPACITY;
 28             //阀值设置为默认容量*默认加载因子
 29             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 30         }
 31         if (newThr == 0) {
 32             float ft = (float)newCap * loadFactor;
 33             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
 34                       (int)ft : Integer.MAX_VALUE);
 35         }
 36         //更新阀值
 37         threshold = newThr;
 38         //根据新的容量创造新的哈希桶
 39         @SuppressWarnings({"rawtypes","unchecked"})
 40             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 41         //更新哈希桶引用
 42         table = newTab;
 43         //旧哈希桶不为空,将旧表中的数据复制到新哈希桶里
 44         if (oldTab != null) {
 45             for (int j = 0; j < oldCap; ++j) {
 46                 Node<K,V> e;
 47                 if ((e = oldTab[j]) != null) {
 48                     //将旧哈希桶里的元素设为null,方便GC
 49                     oldTab[j] = null;
 50                     //若原来链表上只有一个节点(不会发生哈希碰撞)
 51                     if (e.next == null)
 52                         /* 则只需要将其放到新的哈希桶里
 53                         *  桶的下标值是哈希值&(桶的长度-1),由于桶的长度是2的N次方,因此这实际上是个模运算
 54                         *  但是用位运算效率更高
 55                         */
 56                         newTab[e.hash & (newCap - 1)] = e;
 57                     //如果链表的长度超过了阀值则要转为红黑树存储,暂且不讨论
 58                     else if (e instanceof TreeNode)
 59                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
 60                     //链表长度不超过阀值,将旧链表中的节点复制到新链表中
 61                     else { // preserve order
 62                         /*因为容量是翻倍扩大的,因此原链表上的节点可能放在存放在原来的位置上也就是low位
 63                         * 也可能存放在扩容后的下标上high上
 64                         * high = low+原哈希桶容量
 65                         */
 66                         //低位链表头指针和尾指针
 67                         Node<K,V> loHead = null, loTail = null;
 68                         //高位链表头指针和尾指针
 69                         Node<K,V> hiHead = null, hiTail = null;
 70                         Node<K,V> next;
 71                         do {
 72                             next = e.next;
 73                             /*利用位运算判断是放在低位链表还是高位链表
 74                             * 利用哈希值 与 旧的容量,可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,
 75                             * 等于0代表小于oldCap,应该存放在低位,否则存放在高位
 76                             */
 77                             if ((e.hash & oldCap) == 0) {
 78                                 if (loTail == null)
 79                                     loHead = e;
 80                                 else
 81                                     loTail.next = e;
 82                                 loTail = e;
 83                             }
 84                             else {
 85                                 if (hiTail == null)
 86                                     hiHead = e;
 87                                 else
 88                                     hiTail.next = e;
 89                                 hiTail = e;
 90                             }
 91                         } while ((e = next) != null);
 92                         //将低位链表放在原index处
 93                         if (loTail != null) {
 94                             loTail.next = null;
 95                             newTab[j] = loHead;
 96                         }
 97                         //将高位链表放在新index处
 98                         if (hiTail != null) {
 99                             hiTail.next = null;
100                             newTab[j + oldCap] = hiHead;
101                         }
102                     }
103                 }
104             }
105         }
106         return newTab;
107     }
View Code

 

put方法

jdk1.8采取的是“尾插法”,在此之前采用的是“头插法”

java容器三:HashMap源码解析java容器三:HashMap源码解析
 1     public V put(K key, V value) {
 2         return putVal(hash(key), key, value, false, true);
 3     }
 4 
 5     /**
 6      * Implements Map.put and related methods
 7      *
 8      * @param hash hash for key
 9      * @param key the key
10      * @param value the value to put
11      * @param onlyIfAbsent if true, don't change existing value
12      * @param evict if false, the table is in creation mode.
13      * @return previous value, or null if none
14      * jdk1.8以前是头插法,jdk1.8是尾插法
15      * jdk1.8以前也没有转为成红黑树的设置,1.8中当链表长度大于threshold(默认为8)之后会转为红黑树
16      */
17     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
18                    boolean evict) {
19         Node<K,V>[] tab; Node<K,V> p; int n, i;
20         //若哈希桶为空,则直接初始化
21         if ((tab = table) == null || (n = tab.length) == 0)
22             n = (tab = resize()).length;
23         //如果当前index=hash&(n-1)处的节点是空的,代表没有发生哈希碰撞
24         //则直接生成一个新的node挂载上去
25         if ((p = tab[i = (n - 1) & hash]) == null)
26             tab[i] = newNode(hash, key, value, null);
27         else {//发生了哈希碰撞
28             Node<K,V> e; K k;
29             //如果哈希值相同,key值也相同则覆盖
30             if (p.hash == hash &&
31                 ((k = p.key) == key || (key != null && key.equals(k))))
32                 e = p;
33             else if (p instanceof TreeNode)//红黑树暂且不谈
34                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
35             else {//不是覆盖操作,则插入一个普通节点
36                 //遍历链表
37                 for (int binCount = 0; ; ++binCount) {
38                     //找到尾节点
39                     if ((e = p.next) == null) {
40                         //插入节点
41                         p.next = newNode(hash, key, value, null);
42                         //如果追加节点后数量大于8则变成红黑树
43                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
44                             treeifyBin(tab, hash);
45                         break;
46                     }
47                     if (e.hash == hash &&
48                         ((k = e.key) == key || (key != null && key.equals(k))))
49                         break;
50                     p = e;
51                 }
52             }
53             //如果e不是null,说明有需要覆盖的节点
54             if (e != null) { // existing mapping for key
55                 V oldValue = e.value;
56                 if (!onlyIfAbsent || oldValue == null)
57                     //覆盖原来Value并返回OldValue
58                     e.value = value;
59                 //空实现函数,用作LinkedHashMap重写复用
60                 afterNodeAccess(e);
61                 return oldValue;
62             }
63         }
64         ++modCount;
65         //判断size是否需要扩容
66         if (++size > threshold)
67             resize();
68         //空实现函数,用作LinkedHashMap重写复用
69         afterNodeInsertion(evict);
70         return null;
71     }
View Code

 

get方法

java容器三:HashMap源码解析java容器三:HashMap源码解析
 1     public V get(Object key) {
 2         Node<K,V> e;
 3         return (e = getNode(hash(key), key)) == null ? null : e.value;
 4     }
 5 
 6     /**
 7      * Implements Map.get and related methods
 8      *
 9      * @param hash hash for key
10      * @param key the key
11      * @return the node, or null if none
12      */
13     final Node<K,V> getNode(int hash, Object key) {
14         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
15         if ((tab = table) != null && (n = tab.length) > 0 &&
16             (first = tab[(n - 1) & hash]) != null) {
17             if (first.hash == hash && // always check first node
18                 ((k = first.key) == key || (key != null && key.equals(k))))
19                 return first;
20             if ((e = first.next) != null) {
21                 if (first instanceof TreeNode)
22                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
23                 //循环链表,找到key扰动后的哈希值和key值都相等的Node返回
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     }
View Code

 

HashMap与HashTable比较

 1、HashMap允许一个key为null,HashTable不予许

2、HashTable是线程安全的,因为给方法都加了synchronzied关键字

java容器三:HashMap源码解析java容器三:HashMap源码解析
 1     public synchronized V put(K key, V value) {
 2         // Make sure the value is not null
 3         if (value == null) {
 4             throw new NullPointerException();
 5         }
 6 
 7         // Makes sure the key is not already in the hashtable.
 8         Entry<?,?> tab[] = table;
 9         int hash = key.hashCode();
10         int index = (hash & 0x7FFFFFFF) % tab.length;
11         @SuppressWarnings("unchecked")
12         Entry<K,V> entry = (Entry<K,V>)tab[index];
13         for(; entry != null ; entry = entry.next) {
14             if ((entry.hash == hash) && entry.key.equals(key)) {
15                 V old = entry.value;
16                 entry.value = value;
17                 return old;
18             }
19         }
20 
21         addEntry(hash, key, value, index);
22         return null;
23     }
View Code

 

3、HashMap计算桶下标的时候运用了扰乱函数,HashTable直接用key的hashCode值

4、HashMap迭代器是fail-fast迭代器

5、HashMap不能保证随着时间推移元素的次序不变

6、HashMap扩容时扩大一倍,HashTable扩容时扩大一倍+1

7、HashMap中用了许多的位运算来代替HashTable中相同作用的普通乘除运算,效率更高

1)取模桶长度求下标时,用hashCode与(桶长度-1)代替取模运算

2)扩容操作判断放在低位链表还是高位链表时

 1                             /*利用位运算判断是放在低位链表还是高位链表
 2                             * 利用哈希值 与 旧的容量,可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,
 3                             * 等于0代表小于oldCap,应该存放在低位,否则存放在高位
 4                             */
 5                             if ((e.hash & oldCap) == 0) {
 6                                 if (loTail == null)
 7                                     loHead = e;
 8                                 else
 9                                     loTail.next = e;
10                                 loTail = e;
11                             }
12                             else {
13                                 if (hiTail == null)
14                                     hiHead = e;
15                                 else
16                                     hiTail.next = e;
17                                 hiTail = e;
18                             }

 

 

 

注意

1、jdk1.8中链表的长度大于8时就会转化成红黑树存储

2、在jdk1.8以前put方法是采取头插法的,jdk1.8中改成了尾插法

3、HashMap是线程不安全的