• HashMap 阅读


    最近研究了一下java中比较常见的map类型,主要有HashMap,HashTable,LinkedHashMap和concurrentHashMap。这几种map有各自的特性和适用场景。使用方法的话,就不说了,本文重点介绍其原理和底层的实现。文章中的代码来源于jdk1.9版本。

    HashMap特点及原理分析

    特点

    HashMap是java中使用最为频繁的map类型,其读写效率较高,但是因为其是非同步的,即读写等操作都是没有锁保护的,所以在多线程场景下是不安全的,容易出现数据不一致的问题。在单线程场景下非常推荐使用。

    原理

    HashMap的整体结构,如下图所示:
    图片描述

    根据图片可以很直观的看到,HashMap的实现并不难,是由数组和链表两种数据结构组合而成的,其节点类型均为名为Entry的class(后边会对Entry做讲解)。采用这种数据结果,即是综合了两种数据结果的优点,既能便于读取数据,也能方便的进行数据的增删。

    每一个哈希表,在进行初始化的时候,都会设置一个容量值(capacity)和加载因子(loadFactor)。容量值指的并不是表的真实长度,而是用户预估的一个值,真实的表长度,是不小于capacity的2的整数次幂。加载因子是为了计算哈希表的扩容门限,如果哈希表保存的节点数量达到了扩容门限,哈希表就会进行扩容的操作,扩容的数量为原表数量的2倍。默认情况下,capacity的值为16,loadFactor的值为0.75(综合考虑效率与空间后的折衷)。

    • 数据写入。以HashMap(String, String)为例,即对于每一个节点,其key值类型为String,value值类型也为String。在向哈希表中插入数据的时候,首先会计算出key值的hashCode,即key.hashCode()。关于hashCode方法的实现,有兴趣的朋友可以看一下jdk的源码(之前看到信息说有一次面试中问到了这个知识点)。该方法会返回一个32位的int类型的值,以int h = key.hashCode()为例。获取到h的值之后,会计算该key对应的哈希表中的数组的位置,计算方法就是取模运算,h%table.length。因为table的长度为2的整数次幂,所以可以用h与table.length-1直接进行位与运算,即是,index = h & (table.length-1)。得到的index就是放置新数据的位置。
      图片描述
      如果插入多条数据,则有可能最后计算出来的index是相同的,比如1和17,计算的index均为1。这时候出现了hash冲突。HashMap解决哈希冲突的方式,就是使用链表。每个链表,保存的是index相同的数据。

    • 数据读取。从哈希表中读取数据时候,先定位到对应的index,然后遍历对应位置的链表,找到key值和hashCode相同的节点,获取对应的value值。
    • 数据删除。 在hashMap中,数据删除的成本很低,直接定位到对应的index,然后遍历该链表,删除对应的节点。哈希表中数据的分布越均匀,则删除数据的效率越高(考虑到极端场景,数据均保存到了数组中,不存在链表,则复杂度为O(1))。

    JDK源码分析

    构造方法

     1    /**
     2      * Constructs an empty {@code HashMap} with the specified initial
     3      * capacity and load factor.
     4      *
     5      * @param  initialCapacity the initial capacity
     6      * @param  loadFactor      the load factor
     7      * @throws IllegalArgumentException if the initial capacity is negative
     8      *         or the load factor is nonpositive
     9      */
    10     public HashMap(int initialCapacity, float loadFactor) {
    11         if (initialCapacity < 0)
    12             throw new IllegalArgumentException("Illegal initial capacity: " +
    13                                                initialCapacity);
    14         if (initialCapacity > MAXIMUM_CAPACITY)
    15             initialCapacity = MAXIMUM_CAPACITY;
    16         if (loadFactor <= 0 || Float.isNaN(loadFactor))
    17             throw new IllegalArgumentException("Illegal load factor: " +
    18                                                loadFactor);
    19         this.loadFactor = loadFactor;
    20         this.threshold = tableSizeFor(initialCapacity);
    21     }

    从构造方法中可以看到

    • 参数中的initialCapacity并不是哈希表的真实大小。真实的表大小,是不小于initialCapacity的2的整数次幂。
    • 哈希表的大小是存在上限的,就是2的30次幂。当哈希表的大小到达该数值时候,之后就不再进行扩容,只是向链表中插入数据了。

      PUT 方法

     1   /**
     2      * Associates the specified value with the specified key in this map.
     3      * If the map previously contained a mapping for the key, the old
     4      * value is replaced.
     5      *
     6      * @param key key with which the specified value is to be associated
     7      * @param value value to be associated with the specified key
     8      * @return the previous value associated with {@code key}, or
     9      *         {@code null} if there was no mapping for {@code key}.
    10      *         (A {@code null} return can also indicate that the map
    11      *         previously associated {@code null} with {@code key}.)
    12      */
    13     public V put(K key, V value) {
    14         return putVal(hash(key), key, value, false, true);
    15     }
    16 
    17      /**
    18      * Implements Map.put and related methods
    19      *
    20      * @param hash hash for key
    21      * @param key the key
    22      * @param value the value to put
    23      * @param onlyIfAbsent if true, don't change existing value
    24      * @param evict if false, the table is in creation mode.
    25      * @return previous value, or null if none
    26      */
    27     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    28                    boolean evict) {
    29         Node<K,V>[] tab; Node<K,V> p; int n, i;
    30         if ((tab = table) == null || (n = tab.length) == 0)
    31             n = (tab = resize()).length;
    32         if ((p = tab[i = (n - 1) & hash]) == null)
    33             tab[i] = newNode(hash, key, value, null);
    34         else {
    35             Node<K,V> e; K k;
    36             if (p.hash == hash &&
    37                 ((k = p.key) == key || (key != null && key.equals(k))))
    38                 e = p;
    39             else if (p instanceof TreeNode)
    40                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    41             else {
    42                 for (int binCount = 0; ; ++binCount) {
    43                     if ((e = p.next) == null) {
    44                         p.next = newNode(hash, key, value, null);
    45                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    46                             treeifyBin(tab, hash);
    47                         break;
    48                     }
    49                     if (e.hash == hash &&
    50                         ((k = e.key) == key || (key != null && key.equals(k))))
    51                         break;
    52                     p = e;
    53                 }
    54             }
    55             if (e != null) { // existing mapping for key
    56                 V oldValue = e.value;
    57                 if (!onlyIfAbsent || oldValue == null)
    58                     e.value = value;
    59                 afterNodeAccess(e);
    60                 return oldValue;
    61             }
    62         }
    63         ++modCount;
    64         if (++size > threshold)
    65             resize();
    66         afterNodeInsertion(evict);
    67         return null;
    68     }

    可以看到:

    • 给哈希表分配空间的动作,是向表中添加第一个元素触发的,并不是在哈希表初始化的时候进行的。
    • 如果对应index的数组值为null,即插入该index位置的第一个元素,则直接设置tab[i]的值即可。
    • 查看数组中index位置的node是否具有相同的key和hash如果有,则修改对应值即可。
    • 遍历数组中index位置的链表,如果找到了具有相同key和hash的node,跳出循环,进行value更新操作。否则遍历到链表的结尾,并在链表最后添加一个节点,将对应数据添加进去。
    • 方法中涉及到了TreeNode,可以暂时先不关注。

      GET 方法

     1 /**
     2      * Returns the value to which the specified key is mapped,
     3      * or {@code null} if this map contains no mapping for the key.
     4      *
     5      * <p>More formally, if this map contains a mapping from a key
     6      * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     7      * key.equals(k))}, then this method returns {@code v}; otherwise
     8      * it returns {@code null}.  (There can be at most one such mapping.)
     9      *
    10      * <p>A return value of {@code null} does not <i>necessarily</i>
    11      * indicate that the map contains no mapping for the key; it's also
    12      * possible that the map explicitly maps the key to {@code null}.
    13      * The {@link #containsKey containsKey} operation may be used to
    14      * distinguish these two cases.
    15      *
    16      * @see #put(Object, Object)
    17      */
    18     public V get(Object key) {
    19         Node<K,V> e;
    20         return (e = getNode(hash(key), key)) == null ? null : e.value;
    21     }
    22 
    23     /**
    24      * Implements Map.get and related methods
    25      *
    26      * @param hash hash for key
    27      * @param key the key
    28      * @return the node, or null if none
    29      */
    30     final Node<K,V> getNode(int hash, Object key) {
    31         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    32         if ((tab = table) != null && (n = tab.length) > 0 &&
    33             (first = tab[(n - 1) & hash]) != null) {
    34             if (first.hash == hash && // always check first node
    35                 ((k = first.key) == key || (key != null && key.equals(k))))
    36                 return first;
    37             if ((e = first.next) != null) {
    38                 if (first instanceof TreeNode)
    39                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
    40                 do {
    41                     if (e.hash == hash &&
    42                         ((k = e.key) == key || (key != null && key.equals(k))))
    43                         return e;
    44                 } while ((e = e.next) != null);
    45             }
    46         }
    47         return null;
    48     }

    代码分析:

    • 先定位到数组中index位置,检查第一个节点是否满足要求 
    • 遍历对应该位置的链表,找到满足要求节点进行return

    扩容操作

     1  /**
     2      * Initializes or doubles table size.  If null, allocates in
     3      * accord with initial capacity target held in field threshold.
     4      * Otherwise, because we are using power-of-two expansion, the
     5      * elements from each bin must either stay at same index, or move
     6      * with a power of two offset in the new table.
     7      *
     8      * @return the table
     9      */
    10     final Node<K,V>[] resize() {
    11         Node<K,V>[] oldTab = table;
    12         int oldCap = (oldTab == null) ? 0 : oldTab.length;
    13         int oldThr = threshold;
    14         int newCap, newThr = 0;
    15         if (oldCap > 0) {
    16             if (oldCap >= MAXIMUM_CAPACITY) {
    17                 threshold = Integer.MAX_VALUE;
    18                 return oldTab;
    19             }
    20             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    21                      oldCap >= DEFAULT_INITIAL_CAPACITY)
    22                 newThr = oldThr << 1; // double threshold
    23         }
    24         else if (oldThr > 0) // initial capacity was placed in threshold
    25             newCap = oldThr;
    26         else {               // zero initial threshold signifies using defaults
    27             newCap = DEFAULT_INITIAL_CAPACITY;
    28             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    29         }
    30         if (newThr == 0) {
    31             float ft = (float)newCap * loadFactor;
    32             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    33                       (int)ft : Integer.MAX_VALUE);
    34         }
    35         threshold = newThr;
    36         @SuppressWarnings({"rawtypes","unchecked"})
    37             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    38         table = newTab;
    39         if (oldTab != null) {
    40             for (int j = 0; j < oldCap; ++j) {
    41                 Node<K,V> e;
    42                 if ((e = oldTab[j]) != null) {
    43                     oldTab[j] = null;
    44                     if (e.next == null)
    45                         newTab[e.hash & (newCap - 1)] = e;
    46                     else if (e instanceof TreeNode)
    47                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    48                     else { // preserve order
    49                         Node<K,V> loHead = null, loTail = null;
    50                         Node<K,V> hiHead = null, hiTail = null;
    51                         Node<K,V> next;
    52                         do {
    53                             next = e.next;
    54                             if ((e.hash & oldCap) == 0) {
    55                                 if (loTail == null)
    56                                     loHead = e;
    57                                 else
    58                                     loTail.next = e;
    59                                 loTail = e;
    60                             }
    61                             else {
    62                                 if (hiTail == null)
    63                                     hiHead = e;
    64                                 else
    65                                     hiTail.next= e;
    66                                 hiTail = e;}}while((e =next)!=null);if(loTail !=null){
    67                             loTail.next=null;
    68                             newTab[j]= loHead;}if(hiTail !=null){
    69                             hiTail.next=null;
    70                             newTab[j + oldCap]= hiHead;}}}}}return newTab;}

    代码分析:

    • 如果就容量大于0,容量到达最大值,则不扩容。容量未到达最大值,则新容量和新门限翻倍。
    • 如果旧门限和旧容量均为0,则相当于初始化,设置对应的容量和门限,分配空间。
    • 旧数据的整理部分,非常非常的巧妙,先膜拜一下众位大神。在外层遍历node数组,对于每一个table[j],判断该node扩容之后,是属于低位部分(原数组),还是高位部分(扩容部分数组)。判断的方式就是位与旧数组的长度,如果为0则代表的是地位数组,因为index的值小于旧数组长度,位与的结果就是0;相反,如果不为零,则为高位部分数组。低位数组,添加到以loHead为头的链表中,高位数组添加到以hiHead为头的数组中。链表遍历结束,分别设置新哈希表的index位置和(index+旧表长度)位置的值。非常的巧妙。
      注意点
    • HashMap的操作中未进行锁保护,所以多线程场景下存取数据,很存在数据不一致的问题,不推荐使用
    • HashMap中key和value可以为null
    • 计算index的运算,h & (length - 1),感觉很巧妙,学习了
    • 哈希表的扩容中的数据整理逻辑,写的非常非常巧妙,大开眼界


    作者:道可
    链接:https://www.imooc.com/article/details/id/22943
    来源:慕课网
    本文原创发布于慕课网 ,转载请注明出处,谢谢合作

  • 相关阅读:
    2021 省选颓记
    题解 UVA12459 Bees' ancestors
    题解 UVA10812 Beat the Spread!
    题解 UVA12230 过河 Crossing Rivers
    题解 P1850 [NOIP2016 提高组] 换教室
    题解 P1297 [国家集训队]单选错位
    CSP2020 游记
    学习笔记10-18
    【题解-SP1724 TRICOUNT】简单易懂的递推式写法
    题解:论如何利用 大 模 拟 吊打LGJ~
  • 原文地址:https://www.cnblogs.com/wfq9330/p/9562023.html
Copyright © 2020-2023  润新知