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

  • 相关阅读:
    Java基础——数组复习
    JavaScript 笔记总结
    HTML总结
    css代码添加背景图片常用代码
    Java 并发包之线程池综述
    Java 并发之Concurrent 包综述
    AQS 框架之 Lock 接口
    AQS 框架之 LockSupport 线程阻塞工具类
    JAVA 锁之 Synchronied
    ThreadLocal 线程本地变量
  • 原文地址:https://www.cnblogs.com/huanglf714/p/11065742.html
Copyright © 2011-2022 走看看