• HashMap源码分析


    jdk版本1.8

    主要是了解下其内部原理

    一、简介(粗略翻译)

    1. HashMap实现了Map接口。HashMap非线程安全,并且HashMap的Key和Value都可以是null值。其它方面HashMap和HashTable比较像。HashMap不保证元素的顺序。
    2. 如果哈希方法适当的话,其get与put方法是固定的时间复杂度。迭代的时间复杂度是bucket数目加上桶的大小。所以如果对迭代的性能有很高的要求话,则不适合对HashMap赋予很大的存储,或者很低的load factor(装载因子),如果load factor很低,则要么capacity很大跟前面那个一样,相当于对每一个key都做一个哈希,那么当然遍历时间长了;要么size和capactiy都很低,那么就会频繁的扩容,同样也影响性能。
    3. 影响HashMap性能有两个主要的因素,一个是bucket数目,另一个是load factor(装载因子)。load factor用来衡量是否HashMap需要进行扩容。如果Entry的数量超过了 load factor * 当前容量,那么HashMap会进行扩容,有两个步骤一个是rehash,还有个是扩充容量,容量大约是原始容量的两倍。
    4. 一个通用的设定,load factor 为0.75。因为当load factor设定为0.75时,很好的权衡了时间和空间复杂度。更高的load factor减少空间开销但是增加时间复杂度(体现在很多基本方法中,其中包括get 和 put)。为了最小话rehash数量,当设置初始容量时,需要把entry数量和load factor考虑在内。如果capacity > entry_size / load factor, 则不会rehash。
    5. 如果在一个HashMap中有很多映射的时候,相比自动出发rehash扩容HashMap,初始化很大的capacity会有更好的性能表现,因为rehash一次就有一次性能开销,很多key拥有一样的hashcode当然会拉低HashMap的性能表现,要不然HashMap不就和链表一样了嘛?并且为了改善性能,消除打结,当key实现了Comparable,则HashMap能够利用比较来消除打结。
    6. HashMap并非线程安全。如果多线程对同一个HashMap进行结构化操作(增加或者删除映射,改变value不算)那么一定要在外部对HashMap进行同步。经常发生在对Map进行压缩。
    7. 迭代器有fast fail的特性。当一个Map在创建了Iterator之后被结构改变了,迭代器会抛出ConcurrentModificationException。因此当对HashMap进行并发的结构改变时,迭代器会快速无污染的抛出异常,因为在做改变之前就会fail。
    8. 由于fast fail的机制不能被保证在任何情况下都会出现,因此用这种机制写项目是错误的。正确的做法是用此机制来debug。

    二、主要结构分析

    (1)Node。其基本的结构,是一个静态内部类。被很多种entry都用到。注意其hashCode方法并非直接返回Hash。eqauls方法是key 和 value都相等。

     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 
    14         public final K getKey()        { return key; }
    15         public final V getValue()      { return value; }
    16         public final String toString() { return key + "=" + value; }
    17 
    18         public final int hashCode() {
    19            // the hash code is hashcode of key xor hash code of value. Not only hash.
    20             return Objects.hashCode(key) ^ Objects.hashCode(value);
    21         }
    22 
    23         public final V setValue(V newValue) {
    24             V oldValue = value;
    25             value = newValue;
    26             return oldValue;
    27         }
    28 
    29         //key and value are all the same then , the result is true.
    30         public final boolean equals(Object o) {
    31             if (o == this)
    32                 return true;
    33             if (o instanceof Map.Entry) {
    34                 Map.Entry<?,?> e = (Map.Entry<?,?>)o;
    35                 if (Objects.equals(key, e.getKey()) &&
    36                     Objects.equals(value, e.getValue()))
    37                     return true;
    38             }
    39             return false;
    40         }
    41     }

      

    (2)Function hash,也称为扰动函数

    顺便说一下,HashMap的capicity必须是2的n次方。

    1、返回Node的hash值。

    2、并非直接返回key的hash值,因为key的hash值太多了。

    3、计算方法为key的低16位与高16位异或,高16位不变,有人做过实验,这种做法可以减少碰撞而且高效。

    4、为什么HashMap的capitity为2的幂,因为在求桶的位置时,要用长度作为掩码,即index = (n-1)&hash。因此减一就可以获得掩码,即减1位全为1。

    5、如果直接用key的hash和掩码相与时,容易出现碰撞。所以用加上扰动函数产生的掩码可以减少这种情况。

    1  /*
    2     这是hashMap的hash方法,可以看到并不是直接拿key的hashCode
    3     返回的是key的低16位hash值和高16位hash的异或,因为h右移16位后,高位是16个0,任何数和0异或都是其本身
    4 
    5      */
    6     static final int hash(Object key) {
    7         int h;
    8         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    9     }

    (3)Function resize。对HashMap重新分配空间

     1 /**
     2      * 初始化,要么double被大小
     3      */
     4     final Node<K,V>[] resize() {
     5         Node<K,V>[] oldTab = table;
     6         //设置上一个map的capcity,记为oldCap
     7         int oldCap = (oldTab == null) ? 0 : oldTab.length;
     8         //老的threshold. threshold = capacity * load factor, threshold是下一次resize的阈值
     9         int oldThr = threshold;
    10         int newCap, newThr = 0;
    11         //如果不是初始化的话
    12         if (oldCap > 0) {
    13           //如果老的capacity >= 最大的capactiy的话,直接把threshhold设为整数的最大值,也不resize了
    14             if (oldCap >= MAXIMUM_CAPACITY) {
    15                 threshold = Integer.MAX_VALUE;
    16                 return oldTab;
    17             }
    18             //现将新的capacity记为newCap设为oldCap<<1即2*oldCap,然后如果小于最大的capacity并且 oldCap >= 默认的初始Capacity的话
    19             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    20                     oldCap >= DEFAULT_INITIAL_CAPACITY)
    21                 newThr = oldThr << 1; // 将threshold设为两倍
    22         }
    23         //如果oldCap = 0但是oldThr > 0的话,更新newCap
    24         else if (oldThr > 0) // initial capacity was placed in threshold
    25             newCap = oldThr;
    26         //否则newCap为默认初始capacit, 更新newThread
    27         else {               // zero initial threshold signifies using defaults
    28             newCap = DEFAULT_INITIAL_CAPACITY;
    29             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    30         }
    31         //经历了上述所有步骤后,如果newThread还为0的话,更新newThre
    32         if (newThr == 0) {
    33             float ft = (float)newCap * loadFactor;
    34             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    35                     (int)ft : Integer.MAX_VALUE);
    36         }
    37         //更新threshold
    38         threshold = newThr;
    39         @SuppressWarnings({"rawtypes","unchecked"})
    40         //经历了上述很多计算后,初始化了桶。容量就是newCap了。
    41         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    42         table = newTab;
    43         //如果老的table不为空的话,就要进行操作了,老的元素往哪放呢?接着往下看
    44         if (oldTab != null) {
    45             for (int j = 0; j < oldCap; ++j) {
    46                 Node<K,V> e;
    47                 //先得到老的节点,如果老节点不为空
    48                 if ((e = oldTab[j]) != null) {
    49                     //先把老节点设为空
    50                     oldTab[j] = null;
    51                     //如果老节点没有next,那么把它放在新节点的位置。因为newCap变了,所以参与计算的掩码个数变了,自然结果也变了
    52                     if (e.next == null)
    53                         newTab[e.hash & (newCap - 1)] = e;
    54                     //如果老节点为红黑树,那么树进行剪枝
    55                     else if (e instanceof TreeNode)
    56                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    57                     //
    58                     else { // preserve order
    59                         Node<K,V> loHead = null, loTail = null;
    60                         Node<K,V> hiHead = null, hiTail = null;
    61                         Node<K,V> next;
    62                         do {
    63                             next = e.next;
    64                             //根剪枝的判断往哪部分分的方法一样,一部分是low, 详细说一下,如果这个表达式为0的话就,即原来就存在的桶的位置,因为原先参与计算的是(oldCap-1)&hash,即oldCap之后位的都没参加运算
    65                             if ((e.hash & oldCap) == 0) {
    66                                 if (loTail == null)
    67                                     loHead = e;
    68                                 else
    69                                     loTail.next = e;
    70                                 loTail = e;
    71                             }
    72                             //这一部分是新的位置
    73                             else {
    74                                 if (hiTail == null)
    75                                     hiHead = e;
    76                                 else
    77                                     hiTail.next = e;
    78                                 hiTail = e;
    79                             }
    80                         } while ((e = next) != null);
    81                         //把low的这部分保存原位置
    82                         if (loTail != null) {
    83                             loTail.next = null;
    84                             newTab[j] = loHead;
    85                         }
    86                         //把high这部分放入新位置 j+oldcap
    87                         if (hiTail != null) {
    88                             hiTail.next = null;
    89                             newTab[j + oldCap] = hiHead;
    90                         }
    91                     }
    92                 }
    93             }
    94         }
    95         return newTab;
    96     }
    97     

    (4)Function put. 看一下最常用的方法。put

    实际上是putVal。

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

    (5) Function putVal.原来一个put方法这面复杂。

     1 /**
     2      * 传入参数是4个。hash,注意是Node的hash计算方法,然后是K,V,onlyIfAbsent(不存在时候才执行),evict(原注释写的,如果为false,那么Map为创建状态)
     3      *
     4      */
     5 
     6     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
     7                    boolean evict) {
     8         Node<K,V>[] tab; Node<K,V> p; int n, i;
     9         //如果为老的table为空,则resize
    10         if ((tab = table) == null || (n = tab.length) == 0)
    11             n = (tab = resize()).length;
    12         //找到桶的位置,如果为空的话
    13         if ((p = tab[i = (n - 1) & hash]) == null)
    14           //在该位置加入一个新的节点
    15             tab[i] = newNode(hash, key, value, null);
    16         //如果已经有节点存在了
    17         else {
    18             Node<K,V> e; K k;
    19             //如果hash值和头节点的hash相等,并且key的地址和内容都相等,整个if,else就是找e的位置
    20             if (p.hash == hash &&
    21                     ((k = p.key) == key || (key != null && key.equals(k))))
    22                 //e直接等于头节点
    23                 e = p;
    24             //如果有一样不相等并且头节点是红黑树的话,则加入红黑树中
    25             else if (p instanceof TreeNode)
    26                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    27             //如果有一样不相等,并且该节点不属于红黑树的话
    28             else {
    29                 //遍历这个链表,记录位置
    30                 for (int binCount = 0; ; ++binCount) {
    31                     //如果next为空
    32                     if ((e = p.next) == null) {
    33                       //直接加到链表的尾部
    34                         p.next = newNode(hash, key, value, null);
    35                         //如果链表太长了,则生成红黑树
    36                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    37                             treeifyBin(tab, hash);
    38                         break;
    39                     }
    40                     //遍历的过程也有可能遇到两个key相等,也就是找到了
    41                     if (e.hash == hash &&
    42                             ((k = e.key) == key || (key != null && key.equals(k))))
    43                         break;
    44                     p = e;
    45                 }
    46             }
    47             //如果e不为null
    48             if (e != null) { // existing mapping for key
    49                 V oldValue = e.value;
    50                 //改变老的oldValue为新的value
    51                 if (!onlyIfAbsent || oldValue == null)
    52                     e.value = value;
    53                 afterNodeAccess(e);
    54                 return oldValue;
    55             }
    56         }
    57         //如果结构改变了,即增加了元素,则modCount改变,用左fast fail机制
    58         ++modCount;
    59         //判断是否要resize
    60         if (++size > threshold)
    61             resize();
    62         afterNodeInsertion(evict);
    63         return null;
    64     }

    (6)Function get.

    1     /**
    2      * 如果取不到则为null,具体实现在getNode中
    3      */
    4     public V get(Object key) {
    5         Node<K,V> e;
    6         return (e = getNode(hash(key), key)) == null ? null : e.value;
    7     }

    (7)Function getNode

     1     /**
     2      * 参数两个,一个是hash方法后的hash值,另一个是key
     3      */
     4     final Node<K,V> getNode(int hash, Object key) {
     5         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
     6 
     7         //如果table存在并且确实有映射时,并且位置(n-1) & hash 存在时
     8         if ((tab = table) != null && (n = tab.length) > 0 &&
     9             (first = tab[(n - 1) & hash]) != null) {
    10             //如果第一个节点的hash值和目标的哈希值相等并且是一个key时候,返回
    11             if (first.hash == hash && // always check first node
    12                 ((k = first.key) == key || (key != null && key.equals(k))))
    13                 return first;
    14             //否则遍历链表
    15             if ((e = first.next) != null) {
    16                 //如果是红黑树则遍历红黑树寻找
    17                 if (first instanceof TreeNode)
    18                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
    19                 do {
    20                   //否则遍历链表查找
    21                     if (e.hash == hash &&
    22                         ((k = e.key) == key || (key != null && key.equals(k))))
    23                         return e;
    24                 } while ((e = e.next) != null);
    25             }
    26         }
    27         return null;
    28     }
    谢谢!
  • 相关阅读:
    正则去掉 html标签
    app内嵌 h5页面 再滑动的时候 触发击穿底下的一些touchstart事件
    设置按钮不能连续点击并触发点击事件
    使用NPOI导入导出标准Excel
    ASP.NET导出word实例
    常用SQL语句大全总结
    判断以及防止SQL注入
    javascript 正则表达式总结
    前端页面优化技巧
    自我介绍
  • 原文地址:https://www.cnblogs.com/ylxn/p/10108950.html
Copyright © 2020-2023  润新知