zoukankan      html  css  js  c++  java
  • HashMap

    1、HashMap的数据结构
    在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的。HashMap实际上是一个数组和链表的结合体。

    当新建一个hashmap的时候,就会初始化一个数组。

     transient Entry[] table;  
    
    static class Entry<K,V> implements Map.Entry<K,V> {  
            final K key;  
            V value;  
            final int hash;  
            Entry<K,V> next;  
    		..........  
    }  
    

    Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。 

    当往hashmap中put元素时,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位置上已经存放有其他元素了,那么在同一个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的。

    2、hash算法
    在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。hashmap的数据结构是数组和链表的结合,希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是想要的,而不用再去遍历链表。
    首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的。所以Java中是这样做的:首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。

    static int indexFor(int h, int length) {  
           return h & (length-1);  
    } 
    

    看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。

    为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高?
    以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。
    看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

    所以当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
    在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的。

    // Find a power of 2 >= initialCapacity  
    int capacity = 1;  
    while (capacity < initialCapacity)   
           capacity <<= 1; 
    

    3、HashMap的resize 

    当hashmap中的元素越来越多时,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
    那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

    4、key的hashcode与equals方法改写
    hashmap的get方法的过程:首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。所以,hashcode与equals方法对于找到对应元素是两个关键方法。
    Hashmap的key可以是任何类型的对象,但一定要是不可变对象。
    在改写equals方法的时候,需要满足以下三点:
    (1) 自反性:就是说a.equals(a)必须为true。
    (2) 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。
    (3) 传递性:就是说a.equals(b)=true,并且b.equals(c)=true的话,a.equals(c)也必须为true。
    通过改写key对象的equals和hashcode方法,可以将任意的业务对象作为map的key(前提是你确实有这样的需要)。

    5、JDK1.8中对HashMap的优化 

    5、JDK1.8中对HashMap的优化
    (1)、HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的

    当链表长度太长(TREEIFY_THRESHOLD默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能(O(logn))。当长度小于(UNTREEIFY_THRESHOLD默认为6),就会退化成链表。
    HashMap 中关于红黑树的三个关键参数

    TREEIFY_THRESHOLD   桶的树化阈值

    UNTREEIFY_THRESHOLD   树的链表还原阈值  MIN_TREEIFY_CAPACITY   哈希表的最小树形化容量
    static final int TREEIFY_THRESHOLD= 8  static final int UNTREEIFY_THRESHOLD = 6  static final int MIN_TREEIFY_CAPACITY = 64
    当桶中元素个数超过这个值时需要使用红黑树节点替换链表节点 当扩容时,桶中元素个数小于这个值就会把树形的桶元素还原(切分)为链表结构  

    当哈希表中的容量大于这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化
    为了避免进行扩容、树形化选择的冲突,这个值不能小于4 * TREEIFY_THRESHOLD

    (2)、扩容机制

    使用的是2次幂的扩展(指长度扩为原来2倍),所以元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
    图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

    元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

    因此,在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

    这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。

  • 相关阅读:
    1755:菲波那契数列
    1788:Pell数列
    3089:爬楼梯
    7832:最接近的分数
    7649:我家的门牌号
    7216:Minecraft
    7213:垃圾炸弹
    2983:谁是你的潜在朋友
    2723:因子问题
    2722:和数
  • 原文地址:https://www.cnblogs.com/xidian2014/p/10466611.html
Copyright © 2011-2022 走看看