Java集合之HashMap源码解析

前言

在前面的博客中,我写到ArrayList和LinkedList时,他们分别采用数组和双向链表的方式实现,下面我们再一次总结一下,

1.数组,元素顺序插入,寻址快,删除慢,插入慢。

2.双向链表, 元素顺序插入,寻址慢,删除快,插入快。

 

HashMap就是综合以上两种数据结构的优点,即数组+单向链表的方式实现,它是一种K-V键值对的存储结构。

HashMap的基本结构

首先我们先来看一下HashMap中的基本结构,Node是一个静态内部类,它是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 
 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 }

Node类成员属性如下图所示,

Node这个静态内部类,有四个成员属性,我逐一来介绍一下,

1.hash,哈希,即由Key的通过哈希函数得到的哈希码。

2.key,键,数据结构中的键。

3.value,值,数据结构中的值。

4.next,后继结点的引用。

ps,这里只有后继结点,并无前驱结点,这也说明此处是单向链表,并非双向链表。

HashMap自身的成员属性如下:

1 transient Node<K,V>[] table;
2 transient Set<Map.Entry<K,V>> entrySet;
3 transient int size;
4 transient int modCount;
5 int threshold;
6 final float loadFactor;

 HashMap的整体结构示意图如下图所示,

  • ☞哈希桶,也叫bucket,一个bucket对应一个table数组中的一个元素。
  • ☞哈希桶中的普通结点,即:Node<K,V>,一个哈希桶中可以有多个Node,构成单向链表结构。
  • ☞红黑树结构中的黑色结点,当一个哈希桶中的结点个数超过一定的阈值,数据结构由单向的链表结构转成红黑树结构。
  • ☞红黑树结构中的红色结点,当一个哈希桶中的结点个数超过一定的阈值,数据结构由单向的链表结构转成红黑树结构。

HashMap的put方法解读

1 public static void main(String[] args) {
2         HashMap<String, String> map = new HashMap();
3         map.put("test", "我是第一个元素");
4 }

 

第2行,这个一个不带参数的构造函数,初始化增长因子loadFactor=0.75f,

第3行,我们跟一下代码,

1 public V put(K key, V value) {
2         return putVal(hash(key), key, value, false, true);
3 }

 

第2行,跟一下putVal方法,

putVal中的前三个参数分别是,

1.hash(key) →根据key得到hashCode,

2.key→ 键,

3.value→值。

 1  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                    boolean evict) {
 3         Node<K,V>[] tab; Node<K,V> p; int n, i;
 4         if ((tab = table) == null || (n = tab.length) == 0)
 5             n = (tab = resize()).length;
 6         if ((p = tab[i = (n - 1) & hash]) == null)
 7             tab[i] = newNode(hash, key, value, null);
 8         else {
 9             Node<K,V> e; K k;
10             if (p.hash == hash &&
11                 ((k = p.key) == key || (key != null && key.equals(k))))
12                 e = p;
13             else if (p instanceof TreeNode)
14                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
15             else {
16                 for (int binCount = 0; ; ++binCount) {
17                     if ((e = p.next) == null) {
18                         p.next = newNode(hash, key, value, null);
19                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
20                             treeifyBin(tab, hash);
21                         break;
22                     }
23                     if (e.hash == hash &&
24                         ((k = e.key) == key || (key != null && key.equals(k))))
25                         break;
26                     p = e;
27                 }
28             }
29             if (e != null) { // existing mapping for key
30                 V oldValue = e.value;
31                 if (!onlyIfAbsent || oldValue == null)
32                     e.value = value;
33                 afterNodeAccess(e);
34                 return oldValue;
35             }
36         }
37         ++modCount;
38         if (++size > threshold)
39             resize();
40         afterNodeInsertion(evict);
41         return null;
42 }

第3行,声明变量Node<K,V>数组类型的 tab,Node<K,V>的类型的 p,整型n,i

第4-5行,将Map的成员属性table赋值给tab,如果数组table为空或者元素个数为0,那么需要重新扩大table的长度,并且把table的长度记作n,

第6行,新的元素将存储在p链表中,如果p链表为空链表时,进入第7行,反之进入进入第9行,

第8行,根据hash,key,value构建一个新Node<K,V>,并且把i这个位置上的数组的引用指向 新Node。 

第9-35行,新元素将存储的p链表,且p链表不为空时的处理事件。

 

写到这里,我想先引申出两个问题,

1.hash值如何计算?

2.如果确认table数组的索引位置?

 

1.hash值如何计算?

1 static final int hash(Object key) {
2         int h;
3         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
4 }

 如果key为null,那么hash为0 → HashMap的key允许为空,反之 (h = key.hashCode()) ^ (h >>> 16),

1          int h;
2          String key = "test";
3          h = (h = key.hashCode()) ^ (h >>> 16);
4 
5          int a = key.hashCode();
6          int b = h >>> 16;
7          int c = a ^ b;
8          
9          System.out.print(String.format("[h]: %s, [c]:%s", h, c));

 输出结果为 [h]: 3556516, [c]:3556516,

第①步,调用key的hashCode方法,哈希值记作a,

第②步,h>>>16运算,结果记作b,>>>移位运算符代表无符号右移16位,空位以0补齐,

第③步,a与b的异或运算得到c,异或运算代表按位比较,相同则为0,反之为1,比如1^1 = 0,0^0 = 0,1^0 = 1,0^1 = 1。

 总结一下,hash算法的本质就是1.取到key的哈希码 2.高位的移位运算,3.异或运算。

 因此,只要给定两个类型的对象,只要他们的哈希码相同,再经过高位运算,最后经过异或运算得到的hash码值可以断言必然相同,我们把最后得到的hash码记作 int hash,自然想到 hash对 数组的长度 作取模运算,如此,元素最后的分布在数组中会比较的均匀,HashMap却并没有这么做,因为取模运算消耗较大,这也是HashMap源码设计的精妙之处。于是我们再探上述的第二个问题。

 

2.如果确认table数组的索引位置?

上述putVal方法中的 第6行  i = (n - 1) & hash 就是数组的索引位置,根据上下文环境,n为table的长度,初始值为16, hash为上述分三部得出的哈希码。最后(n-1)和hash “&”。&是二进制中的与运算,举个例子,

0&0=0,0&1=0,1&0=0,1&1=1,即按位运算的规则为同时为1,值才为1,反之为0。

 

数组的扩容保证长度length为2的n次方,因此当length为2的n次方时,(length-1)与hash作 与运算  等价于  hash%lengh,即hash对数组长度取模,要证明这一点,不难。

举个栗子,length长度是2的4次方,即length=16,二进制为10000,length-1  为 1111,hash我们就取上述算出的hash :1101100100010010100100,与运算如下图所示,

得到的结果为100 十进制为4,我们再看取模运算,3556516 % 16 = 4

那为什么要使用与运算来替代取模运算呢?因为,&比%具有更高的效率。

 

 

讨论完引申出来的两个问题,再回到putVal方法,第9-35行,新元素将存储的p链表,且p链表不为空时的处理事件。该事件有三类情况,

第①类:待新增的元素,和哈希桶中链表的第一个元素比较,两者的key相等,则新值替换旧值,代码第10-12行,第29-34行。

第②类:待新增的元素,所在的哈希桶的数据结构如果是红黑树结构,即TreeNode,和之前TreeMap源码中元素新增是非类似,不作详细介绍。

第③类,待新增的元素,和哈希桶中的第一个值比较,两者key不同于是逐个比较,如果找到相同的key,则新值替换旧值,反之,新元素追加到链表尾部。

 

最后modCount自增,size自增。HashMap的put方法算是讲完辣。接下来我们再看看HashMap的无参构造函数,

1     /**
2      * Constructs an empty <tt>HashMap</tt> with the default initial capacity
3      * (16) and the default load factor (0.75).
4      */
5     public HashMap() {
6         this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
7    }

第2-3行,注释翻译过来就是无参的构造函数,默认的容量capacity 16,和 默认的装载因子loadFactor  0.75d,

第6行,装载因子默认值为0.75d,

第6行,all other fileds defaulted? 既然叫[defaulted],您的意思是其他成员属性在此初始化了的?我表示看不见。

 

这是否说明一个问题?不恰当的注释容易给阅读者带来误导,楼主我在软件开发中,往往偏向于:做可读性更高的优雅代码,不太偏向于做复杂的注释。

好啦,回到问题本身,HashMap默认是容量初始化,在put方法的resize方法中,代码如下

1  newCap = DEFAULT_INITIAL_CAPACITY;
2  newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

HashMap的容量初始为16,需要扩容的阈值: 容量 * 装载因子,即16*0.75,即当下一次新增元素超过了 原容量的0.75倍,进行一次扩容,每一次扩容,原容量的两倍增长。

 

 

HashMap的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 }

 和put方法一致,同样调用hash()函数,根据key,三步计算得到哈希码。跟一下removeNode方法,

 1 final Node<K,V> removeNode(int hash, Object key, Object value,
 2                                boolean matchValue, boolean movable) {
 3         Node<K,V>[] tab; Node<K,V> p; int n, index;
 4         if ((tab = table) != null && (n = tab.length) > 0 &&
 5             (p = tab[index = (n - 1) & hash]) != null) {
 6             Node<K,V> node = null, e; K k; V v;
 7             if (p.hash == hash &&
 8                 ((k = p.key) == key || (key != null && key.equals(k))))
 9                 node = p;
10             else if ((e = p.next) != null) {
11                 if (p instanceof TreeNode)
12                     node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
13                 else {
14                     do {
15                         if (e.hash == hash &&
16                             ((k = e.key) == key ||
17                              (key != null && key.equals(k)))) {
18                             node = e;
19                             break;
20                         }
21                         p = e;
22                     } while ((e = e.next) != null);
23                 }
24             }
25             if (node != null && (!matchValue || (v = node.value) == value ||
26                                  (value != null && value.equals(v)))) {
27                 if (node instanceof TreeNode)
28                     ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
29                 else if (node == p)
30                     tab[index] = node.next;
31                 else
32                     p.next = node.next;
33                 ++modCount;
34                 --size;
35                 afterNodeRemoval(node);
36                 return node;
37             }
38         }
39         return null;
40 }

第5行,index = (n - 1) & hash],哈希桶的寻址和put方法一致。

第4-24行,找出待删除的结点。

第27-32行,删除结点Node,并返回该结点。

这里没什么太多可以讲解的,也就不逐行分析了。

 

 

 

posted @ 2018-06-17 20:57  冰糖小城  阅读(405)  评论(0编辑  收藏  举报