zoukankan      html  css  js  c++  java
  • 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,并返回该结点。

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

     

  • 相关阅读:
    关于iOS开发property with 'retain(or strong)' attribute must be of object type
    机器学习之神经网络
    一些知名的开源社区
    机器学习之正则化
    机器学习之逻辑回归(logistic回归)
    机器学习之正规方程法
    机器学习之线性回归、多项式回归
    机器学习之梯度下降法
    64位windows7下安装python,配置numpy和matplotlib库
    mysql分区查询
  • 原文地址:https://www.cnblogs.com/sunshine798798/p/9124971.html
Copyright © 2011-2022 走看看