JAVA数据结构——Map之HashMap

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA数据结构——Map之HashMap相关的知识,希望对你有一定的参考价值。

JAVA数据结构——Map之HashMap

 

一、原型及简介

  原型:public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

  简介:HashMap基于散列表实现的一个key-value数据结构,能够实现通过key值快速查找。HashMap继承自AbstractMap抽闲类,实现了Map接口。

 二、数据结构原理介绍

  如下图所示,HashMap是利用数组与链表结合的形式构建的。竖列为数组结构,默认初始数量为16(1<<4)个,横列为链表结构用于解决散列冲突的问题。当数组中有值得元素超过了装载因子的比例(默认为0.75)时,会引发扩容的操作。此操作是为了避免元素过满时引起的链表长度过长,从而影响查找性能。

  技术分享图片

上图为jdk1.7之前的实现,jdk1.8实现方法是当某一个桶中的元素个数超过了8时,将此桶中的链表构建成红黑树。

三、常用源码解析

  1、常量说明

技术分享图片
 1     /**
 2      * 默认初始容量
 3      */
 4     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 5 
 6     /**
 7      * 最大元素数量
 8      */
 9     static final int MAXIMUM_CAPACITY = 1 << 30;
10 
11     /**
12      * 默认装载因子
13      */
14     static final float DEFAULT_LOAD_FACTOR = 0.75f;
15 
16     /**
17      * 当一个桶中的元素的数量大于8时,该链表结构可能被转化成一棵红黑树,优化查找
18      */
19     static final int TREEIFY_THRESHOLD = 8;
20 
21     /**
22      * 当一个桶中的元素的数量小于6时,该树结构被转化成链表。
23      */
24     static final int UNTREEIFY_THRESHOLD = 6;
25 
26     /**
27      * 桶被树化的另一个条件是,当hashmap中元素个数大于4 * MIN_TREEIFY_CAPACITY 。避免调整大小和treei阈值之间的冲突。
28      */
29     static final int MIN_TREEIFY_CAPACITY = 64;
常量值说明

  2、变量说明

技术分享图片
 1     /**
 2      * The table, initialized on first use, and resized as
 3      * necessary. When allocated, length is always a power of two.
 4      * (We also tolerate length zero in some operations to allow
 5      * bootstrapping mechanics that are currently not needed.)
 6      */
 7     transient Node<K,V>[] table;
 8 
 9     /**
10      * Holds cached entrySet(). Note that AbstractMap fields are used
11      * for keySet() and values().
12      */
13     transient Set<Map.Entry<K,V>> entrySet;
14 
15     /**
16      * HashMap中当前元素个数
17      */
18     transient int size;
19 
20     /**
21      * HashMap对象被修改次数,防止出现多个线程修改出现的线程不一致性,每次修改HashMap的值时,都会自增。当使用Iterator操作HashMap时,会用此值与Iterator内部的值做一次比较,从而判断HashMap有没有被其他线程修改。故建议每次遍历HashMap时都使用Iterator。
22      */
23     transient int modCount;
24 
25     /**
26      * 装载因子
27      */
28     final float loadFactor;
变量值说明

  漏了一个变量:threshold,代表着扩容的阈值,其值为  当前容量*装载因子

  3、节点数据结构

技术分享图片
 1     static class Node<K,V> implements Map.Entry<K,V> {
 2         final int hash; //散列码
 3         final K key;  //key值
 4         V value;  //value值
 5         Node<K,V> next;  //链表结构指针,指向下一节点
 6 
 7         Node(int hash, K key, V value, Node<K,V> next) {
 8             this.hash = hash;
 9             this.key = key;
10             this.value = value;
11             this.next = next;
12         }
13 
14         public final K getKey()        { return key; }
15         public final V getValue()      { return value; }
16         public final String toString() { return key + "=" + value; }
17 
18         // 返回该节点的散列码
19         public final int hashCode() {
20             // key值的散列码 幂运算 value值得散列码
21             // 散列函数为空值返回0,非空值则返回该对象的32位JVM地址
22             return Objects.hashCode(key) ^ Objects.hashCode(value);
23         }
24 
25         public final V setValue(V newValue) {
26             V oldValue = value;
27             value = newValue;
28             return oldValue;
29         }
30 
31         public final boolean equals(Object o) {
32             if (o == this)
33                 return true;
34             if (o instanceof Map.Entry) {
35                 Map.Entry<?,?> e = (Map.Entry<?,?>)o;
36                 if (Objects.equals(key, e.getKey()) &&
37                     Objects.equals(value, e.getValue()))
38                     return true;
39             }
40             return false;
41         }
42     }
节点数据结构

  4、常用方法

 

技术分享图片
 1     /**
 2      * 指定初始大小以及装载因子
 3      */
 4     public HashMap(int initialCapacity, float loadFactor) {
 5         if (initialCapacity < 0)
 6             throw new IllegalArgumentException("Illegal initial capacity: " +
 7                                                initialCapacity);
 8         if (initialCapacity > MAXIMUM_CAPACITY)
 9             initialCapacity = MAXIMUM_CAPACITY;
10         if (loadFactor <= 0 || Float.isNaN(loadFactor))
11             throw new IllegalArgumentException("Illegal load factor: " +
12                                                loadFactor);
13         this.loadFactor = loadFactor;
14         this.threshold = tableSizeFor(initialCapacity);
15     }
16 
17     /**
18      * 返回一个比cap大的最小的2的幂次方整数
19      */
20     static final int tableSizeFor(int cap) {
21         int n = cap - 1;
22         n |= n >>> 1;
23         n |= n >>> 2;
24         n |= n >>> 4;
25         n |= n >>> 8;
26         n |= n >>> 16;
27         return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
28     }
构造方法

因为HashMap的容量必须是2的幂次方,所以构造方法中有关于tableSizeFor方法,获得比给定容量大的最小的2的幂次方整数,很霸气的算法,其具体的说明可参考链接:

【转载】http://blog.csdn.net/fan2012huan/article/details/51097331(写的很详细,很好)。

 

技术分享图片
  1     /**
  2      * 将key-value键值对放入HashMap中
  3      */
  4     public V put(K key, V value) {
  5         return putVal(hash(key), key, value, false, true);
  6     }
  7 
  8     /**
  9      * 实际put的方法
 10      */
 11     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 12                    boolean evict) {
 13         Node<K,V>[] tab; //暂存HashMap节点数组
 14         Node<K,V> p; //暂存本次要插入的节点元素数据
 15         int n, i;
 16 
 17         //如果当前HashMap为空,则计算新分配空间
 18         if ((tab = table) == null || (n = tab.length) == 0)
 19             n = (tab = resize()).length;
 20         // 如果计算出的新节点位置(hash & (n - 1) 等价于 hash % n)是空,则将元素直接放入
 21         if ((p = tab[i = (n - 1) & hash]) == null)
 22             tab[i] = newNode(hash, key, value, null);
 23         //插入新的节点,并重新组织HashMap(确定位置并决定是否树化)
 24         else {
 25             Node<K,V> e; K k;
 26             // 插入了重复的key值(hash码一致且key值一致)
 27             if (p.hash == hash &&
 28                 ((k = p.key) == key || (key != null && key.equals(k))))
 29                 e = p;
 30             // 如果p是红黑树,则执行红黑树的插入操作
 31             else if (p instanceof TreeNode)
 32                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
 33             // 此分支代表了链表的插入操作
 34             else {
 35                 for (int binCount = 0; ; ++binCount) {
 36                     // 到达链表尾端,则执行插入
 37                     if ((e = p.next) == null) {
 38                         //插入
 39                         p.next = newNode(hash, key, value, null);
 40                         如果节点数量超过阈值,则执行树化操作
 41                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
 42                             treeifyBin(tab, hash);
 43                         break;
 44                     }
 45                     // 插入了重复值
 46                     if (e.hash == hash &&
 47                         ((k = e.key) == key || (key != null && key.equals(k))))
 48                         break;
 49                     p = e;
 50                 }
 51             }
 52             if (e != null) { // existing mapping for key
 53                 V oldValue = e.value;
 54                 if (!onlyIfAbsent || oldValue == null)
 55                     e.value = value;
 56                 afterNodeAccess(e);
 57                 return oldValue;
 58             }
 59         }
 60         ++modCount;
 61         //如果元素个数超过阈值,则重新分配空间,并组织数据结构
 62         if (++size > threshold)
 63             resize();
 64         afterNodeInsertion(evict);
 65         return null;
 66     }
 67 
 68     /**
 69      * 针对每个桶重新分配空间
 70      */
 71     final Node<K,V>[] resize() {
 72         Node<K,V>[] oldTab = table; //暂存当前table结构
 73         int oldCap = (oldTab == null) ? 0 : oldTab.length; //暂存当前桶的数量
 74         int oldThr = threshold; //暂存扩容的阈值
 75         int newCap, newThr = 0; //定义新的容量和阈值
 76         // 如果原有HashMap不为空
 77         if (oldCap > 0) {
 78             //如果容量已经达到了上限,则不扩容,返回原oldTab
 79             if (oldCap >= MAXIMUM_CAPACITY) {
 80                 threshold = Integer.MAX_VALUE;
 81                 return oldTab;
 82             }
 83             //如果容量没有达到上限,则将容量及扩容阈值均翻倍
 84             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 85                      oldCap >= DEFAULT_INITIAL_CAPACITY)
 86                 newThr = oldThr << 1; // double threshold
 87         }
 88         // 容量为0但老的阈值大于0,则阈值保持不变
 89         else if (oldThr > 0) // initial capacity was placed in threshold
 90             newCap = oldThr;
 91         // 如果容量与阈值均为0,则执行初始化
 92         else {               // zero initial threshold signifies using defaults
 93             newCap = DEFAULT_INITIAL_CAPACITY;//容量为默认容量
 94             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//阈值为默认容量*默认阀值(16*0.75)
 95         }
 96         // 如果现有容量翻倍后大于最大容量或现有容量小于系统默认值(16),才会出现新阈值=0的情况,
 97         if (newThr == 0) {
 98             float ft = (float)newCap * loadFactor;
 99             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
100                       (int)ft : Integer.MAX_VALUE);
101         }
102 
103         // 设置最新的扩容阈值
104         threshold = newThr;
105 
106         // 创建扩容后的桶数组
107         @SuppressWarnings({"rawtypes","unchecked"})
108             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
109         table = newTab;
110 
111         //重新组织每个桶内的链表或树状结构
112         if (oldTab != null) {
113             //遍历每个桶,分别处理每个桶中的数据
114             for (int j = 0; j < oldCap; ++j) {
115                 Node<K,V> e;
116 
117                 //当前桶不为空,则需将oldTab中的内容组织到newTab中
118                 if ((e = oldTab[j]) != null) {
119                     oldTab[j] = null;
120                     if (e.next == null) //e没有子节点,则根据e的hash值直接将此节点放到扩容后的桶数组中合适位置
121                         // 此处e.hash & (newCap - 1)等价于e.hash % newCap
122                         newTab[e.hash & (newCap - 1)] = e;
123                     else if (e instanceof TreeNode) //如果e是个树型节点,则遍历红黑树,将树中的每个节点放到新的桶数组中合适的位置,并根据新的结构决定是否需要将每个桶做树化
124                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
125                     else { // e是个链式节点
126                         Node<K,V> loHead = null, loTail = null;
127                         Node<K,V> hiHead = null, hiTail = null;
128                         Node<K,V> next;
129                         do {
130                             next = e.next;
131                             // 因为容量扩大了二倍,则元素要么保持不变,要么放到index + oldCap位置
132                             if ((e.hash & oldCap) == 0) {// 元素位置保持不变,先将元素放到lo链表中
133                                 if (loTail == null)
134                                     loHead = e;
135                                 else
136                                     loTail.next = e;
137                                 loTail = e;
138                             }
139                             else {// 元素位置需要移动,先将元素放到hi链表中
140                                 if (hiTail == null)
141                                     hiHead = e;
142                                 else
143                                     hiTail.next = e;
144                                 hiTail = e;
145                             }
146                         } while ((e = next) != null);
147                         if (loTail != null) {//将lo链表放到newTab中原来(j)的位置
148                             loTail.next = null;
149                             newTab[j] = loHead;
150                         }
151                         if (hiTail != null) {//将hi链表放到newTab中扩容(j+oldCap)的位置
152                             hiTail.next = null;
153                             newTab[j + oldCap] = hiHead;
154                         }
155                     }
156                 }
157             }
158         }
159     
160         //返回最新的结构
161         return newTab;
162     }
public V put(K key, V value)

put方法包含了HashMap的实际初始化及构建的过程,仔细研究put方法,可以更好的了解HashMap这种数据结构

 

技术分享图片
 1     /**
 2      * 根据key值获取value值
 3      */
 4     public V get(Object key) {
 5         Node<K,V> e;
 6         return (e = getNode(hash(key), key)) == null ? null : e.value;
 7     }
 8 
 9     /**
10      * 根据哈希码及key值获取value值
11      */
12     final Node<K,V> getNode(int hash, Object key) {
13         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
14 
15         //如果表不为空且表的长度不为空且根据hash码定位到桶不为空
16         if ((tab = table) != null && (n = tab.length) > 0 &&
17             (first = tab[(n - 1) & hash]) != null) {
18 
19             //如果该桶的第一个元素hash码与传参相同且key值也相同,则返回该元素节点
20             if (first.hash == hash && // always check first node
21                 ((k = first.key) == key || (key != null && key.equals(k))))
22                 return first;
23 
24             //如果该节点的下一个下一个节点不为空
25             if ((e = first.next) != null) {
26                 //如果该节点是树形节点,则遍历红黑树查找匹配节点
27                 if (first instanceof TreeNode)
28                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
29 
30                 //如果是链式节点,则遍历该链表查找匹配节点
31                 do {
32                     if (e.hash == hash &&
33                         ((k = e.key) == key || (key != null && key.equals(k))))
34                         return e;
35                 } while ((e = e.next) != null);
36             }
37         }
38         return null;
39     }
public V get(Object key)

 

以上是关于JAVA数据结构——Map之HashMap的主要内容,如果未能解决你的问题,请参考以下文章

Java中常见数据结构Map之HashMap

Java集合分析之Map-从HashMap说起

java集合之Map接口

java集合系列之HashMap源码

java集合之HashMap

java之Map源代码浅析