zoukankan      html  css  js  c++  java
  • HashMap原理

    HashMap内部数据结构
     
         HashMap内部采用数组和链表结合的方式来存取数据(见下图)。这种方式有什么好处呢? 我们知道,数组操作对于检索是O(1)的,能够很快的根据数组的下标定位对象,但是插入和删除操作不高,会引起后续节点的移动,而链表的优势是:插入和删除非常的迅速,只需要重设相应的next指针就可以了。因此HashMap是结合两者优势,这是一种折中的方案。(在java8中引入了红黑树,是对性能的更进一步优化)
     
    放到HashMap的key-value对是如何保存的?
     
         HashMap类内部有一个静态内部类Entry,它继承自Map.Entry接口(不要混淆了HashMap.Entry和Map.Entry):
    static class Entry <K , V> implements Map .Entry < K, V > { ... }
     
    再看看HashMap内部有这么一个Entry数组:transient Entry[] table; 当我们向HashMap中put一个key-value时,HashMap会将它转换为Entry对象并存放到Entry数组中的某个位置(由hash函数计算hash值后用数组长度取模来决定存放位置的)。
     
     
    HashMap内部几个重要的方法解析:
     
    首先欢迎hash(int h)方法闪亮登场.... 
    各位码农好,我是HashMap内部的hash方法,简单介绍一下自己:我是根据key.hashCode值进行重新计算的一个方法,因为我不是很相信他们,我相信我能做得更好:
    复制代码
    static int hash( int h ) {
                 // This function ensures that hashCodes that differ only by
                 // constant multiples at each bit position have a bounded
                 // number of collisions (approximately 8 at default load factor).
                 h ^= (h >>> 20) ^ ( h >>> 12) ;
                 return h ^ (h >>> 7 ) ^ (h >>> 4);
           } 
    复制代码

    说明:该hash算法的内部实现乍一看的确很晦涩难懂。参数值h是key.hashCode(),很明显,该方法的意图就是根据key.hasCode()的值进行重新计算。为什么要这么做呢?因为HashMap要求key的hashCode值分布越均匀性能就越好(见上图,若hash函数计算出的hash值越均匀,链表的长度也就越均衡,就不会造成有的链表长度很短,而有的链表长度特别长的情况,我们知道,链表长度越长,检索效率就越慢)。而put到HashMap作为key的对象的hashCode可能五花八门,有的对象的hashCode可能还进行过重写,这可能导致put到HashMap中的key的hashCode值分布可能会不均衡,所以HashMap就利用这样一个算法对hashCode进行重新计算,以期望得到更加分布均衡的hashCode值。

     
    (上述关于均衡的hashCode值原理待详细讲解)
     
    再欢迎我们第二个嘉宾indexFor方法:
    大家好,我也简单介绍一下自己,我是根据hash值和Entry[]的长度进行取模运算的,是不是很奇怪我为什么长成这个样子,其实我也就是伪装很好的”取模“运算而已。我觉得这样比较酷一点,而且我办事效率比其他那些取模方法要快的多。
    static int indexFor( int h , int length) {
                 return h & (length - 1 );
           }

    说明:在计算机数学里有这么一个法则: 当SIZE满足大小是2的幂的时候,X % SIZE(取模操作)和 X & (SIZE-1)是等价的,不信?自己动手演算一下?因此我们知道了为什么HashMap要求我们提供的capacity大小是2的幂的原因了吧?用户提供给HashMap构造函数的capacity值可能不是2的幂,所以hashmap内部进行了验证并将其重设为最接近指定capacity值的2的幂的那个值:

    int capacity = 1;
    while (capacity < initialCapacity)           
         capacity <<= 1;

    介绍了前两个重要方法,下面欢迎我们熟悉的put方法出场:

    复制代码
    public V put ( K key , V value ) {
                 if ( key == null)
                       return putForNullKey ( value) ;
                 int hash = hash (key . hashCode()) ;
                 int i = indexFor (hash , table . length) ;
                 for ( Entry <K , V> e = table [ i ]; e != null ; e = e. next ) {
                       Object k ;
                       //
                       if ( e .hash == hash && ((k = e .key ) == key || key .equals ( k))) {
                            V oldValue = e. value ;
                             e .value = value ;
                             e .recordAccess ( this) ;
                             return oldValue ;
                       }
                 }
                 modCount ++;
                 addEntry (hash , key , value , i ) ;
                 return null ;
           }
    复制代码
    说明:有了对hash和indexFor方法以及对前面介绍的HashMap内部数据结构的知识,理解put操作就变得非常容易了,首先判断key是不是null(HashMap是允许用null作为key),如果是就将它插入到table[0]处,否则,就先以key.hashCode()为基础调用HashMap#hash方法得到一个分布更好的hash值,然后对该值取模(indexFor)得到相应的数组位,然后,判断该数组位之前有没有存放过Entry对象,如果没有就创建一个Entry对象插入到该位置,如果该位置已经有Entry对象了,那么就依次从链表的头部开始查找有没有相同key的Entry对象,如果匹配到,就替换匹配项的值,否则,就将这个Entry元素插入到链表的最后面。理解了put操作,get操作就不再赘述了。
     
    HashMap对容量的动态扩容
     
         HashMap默认容量是16(也就是初始数组(也叫桶,bucket)大小是16),内部有一个变量DEFAULT_LOAD_FACTOR(负载因子,默认0.75)就是用来指示自动扩展大小的阀值,也就是说,当我们往数组中添加的元素个数大于capacity * 0.75(3/4)的时候,HashMap会自动调用resize方法调整bucket的大小。调整大小的过程就是新建一个更大的Entry数组,然后将之前添加过的Entry对象全部复制过去,由于每个Entry对象都保存有之前计算好的hash值,当数组容量扩大时再次取模(hash % length)得到的位置就不同了,所以原本在同一个链表中的元素就可能分开了,调整容量也就是为了实现将链表长度缩短这一目的的。具体复制操作的实现见transfer方法,这也很容易理解。 
     
    关于ConcurrentModificationException
     
         HashMap类是非线程安全的,当一个线程正在对HashMap对象进行迭代操作,另一个线程对HashMap对象进行了更改,或者在单个线程下,用户在迭代的循环体中不正确的更改了HashMap对象元素的大小,都经常会抛出ConcurrentModificationException异常,这就是我们说的fail-fast,迭代时,其内部处理逻辑是这样的,HashMap内部有一个用以指示修改次数的计数器(modCount),当每次进行put,remove等操作时,都会引起计数器发生变化,每次迭代开始之前会取出modCount值存放到另一个exceptedModCount局部变量中,然后每个迭代过程都会检查该exceptedModCount是否和当前modCount相等,如果不等就会抛出ConcurrentModificationException异常,因此,在有多线程并发访问的情况下,建议使用ConcurrentHashMap这个线程安全类来替代HashMap的使用(题外:使用线程安全类就能确保对它的所有操作都是线程安全的吗?答案是否定的。这属于多线程原子操作等考虑的范围,不在本章详细讨论)。
  • 相关阅读:
    css3之box-shadow
    css3之圆角
    KOA 学习(九)koa-static
    KOA 学习(八) koa-bodyparser
    KOA 学习(七) 路由koa-router
    videojs使用的常见问题
    KOA 学习(六)superAgent
    KOA 学习(四)
    Ng第五课:Octave 教程(Octave Tutorial)
    Ng第四课:多变量线性回归(Linear Regression with Multiple Variables)
  • 原文地址:https://www.cnblogs.com/zpbolgs/p/6262379.html
Copyright © 2011-2022 走看看