zoukankan      html  css  js  c++  java
  • java容器三:HashMap源码解析

    前言:Map接口

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

    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内部是采用“拉链法”来实现,示意图如下

    确定桶下标

    确定桶下标也就是确定键值对保存在数组中的下标,是根据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

    扩容方法源码解析

      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采取的是“尾插法”,在此之前采用的是“头插法”

     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方法

     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关键字

     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是线程不安全的

  • 相关阅读:
    MSDN Magazine搞错了
    Visual Studio 2005中设置调试符号(Debug Symbols)
    BCB 6的问题
    吴裕雄天生自然Spring Boot使用Spring Data JPA实现人与身份证的一对一关系映射
    吴裕雄天生自然Spring BootSpring Data JPA
    吴裕雄天生自然Spring BootSpring Boot对JSP的支持
    吴裕雄天生自然Spring BootSpring Boot的异常统一处理
    吴裕雄天生自然Spring Boot使用Spring Data JPA实现Author与Article的一对多关系映射
    吴裕雄天生自然Spring Boot解决 Error creating bean with name 'entityManagerFactory' defined in class path resource
    吴裕雄天生自然Spring Boot@ExceptionHandler注解和@ControllerAdvice注解
  • 原文地址:https://www.cnblogs.com/huanglf714/p/11065742.html
Copyright © 2011-2022 走看看