• 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不会倒置。

  • 相关阅读:
    盒子垂直水平居中
    Sahi (2) —— https/SSL配置(102 Tutorial)
    Sahi (1) —— 快速入门(101 Tutorial)
    组织分析(1)——介绍
    Java Servlet (1) —— Filter过滤请求与响应
    CAS (8) —— Mac下配置CAS到JBoss EAP 6.4(6.x)的Standalone模式(服务端)
    JBoss Wildfly (1) —— 7.2.0.Final编译
    CAS (7) —— Mac下配置CAS 4.x的JPATicketRegistry(服务端)
    CAS (6) —— Nginx代理模式下浏览器访问CAS服务器网络顺序图详解
    CAS (5) —— Nginx代理模式下浏览器访问CAS服务器配置详解
  • 原文地址:https://www.cnblogs.com/xidian2014/p/10466611.html
Copyright © 2020-2023  润新知