HashMap使用经验(下)

Posted 麦克叔叔每晚10点说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap使用经验(下)相关的知识,希望对你有一定的参考价值。

HashMap使用经验(下)

对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用hash(int h)方法所计算得到的Hash码值总是相同的。接下来程序会调用indexFor(int h, int length)方法来计算该对象应该保存在table数组的哪个索引处。indexFor(int h, int length)方法的代码如清单3-48所示。


代码清单3-48 HashMap的indexFor方法源代码


static int indexFor(int h, int length)   

{   

    return h & (length-1);   

}

这个方法非常巧妙,它总是通过h&(table.length-1)来得到该对象的保存位置,而HashMap底层数组的长度总是2的n次方,当length总是2的倍数时,h&(length-1)是一个非常巧妙的设计:假设 h=5,length=16,那么h&length-1将得到5;如果h=6,length=16,那么h&length-1将得到6,如果h=15,length=16,那么h&length-1将得到15;但是当h=16时,length=16时,那么h&length-1将得到0了;当h=17时,length=16时,那么h&length-1是1,这样保证计算得到的索引值总是位于table数组的索引之内。


根据上面3-46所示的put方法源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该key的hashCode()返回值决定该Entry的存储位置,如果两个 Entry的key的hashCode()返回值相同,那它们的存储位置相同。如果这两个Entry的key通过 equals比较返回true,新添加Entry的Value将覆盖集合中原有Entry的Value,但key不会被覆盖。如果这两个Entry的key通过equals比较返回false,新添加的Entry将与集合中原有Entry形成Entry链,而且新添加的Entry位于Entry链的头部。


当向HashMap中添加key-value对,由其key的hashCode()返回值决定该key-value对(就是Entry对象)的存储位置。当两个Entry对象的key的hashCode()返回值相同时,将由key通过eqauls()比较值决定是采用覆盖行为(返回true),还是产生Entry链(返回false)。


清单3-46所示代码中也调用了addEntry(hash, key, value, i);方法,addEntry是HashMap提供的一个包访问权限的方法,该方法仅用于添加一个key-value 对。代码如清单3-49所示。

代码清单3-49 HashMap的addEntry方法源代码


void addEntry(int hash, K key, V value, int bucketIndex)

{

    // 获取指定 bucketIndex 索引处的 Entry

    // table是一个普通数组,每个数组都有一个固定的长度,这个数组的长度就是HashMap的容量。

    Entry<K,V> e = table[bucketIndex];     //

    // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry

    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);

    // 如果 Map 中的 key-value 对的数量超过了极限

    //Size变量用于保存该 HashMap 中所包含的 key-value 对的数量。

    //threshold变量包含了HashMap能容纳的key-value对的极限,它的值等于HashMap的容量乘以负载因子(load factor)。

    //当size++>= threshold时,HashMap 会自动调用resize方法扩充HashMap的容量。每扩充一次,HashMap 的容量就增大一倍。

    if (size++ >= threshold)

        // 把 table 对象的长度扩充到 2 倍

        resize(2 * table.length);

}


系统总是将新添加的Entry对象放入table数组的bucketIndex索引处,如果bucketIndex索引处已经有了一个Entry对象,那新添加的Entry对象指向原有的Entry对象(产生一个Entry链),如果bucketIndex索引处没有Entry对象,那么通过代码Entry<K,V>e=table[bucketIndex];确保e变量是null,也就是新放入的Entry对象指向Null,也就是没有产生Entry链。


当HashMap的每个bucket里存储的Entry只是单个Entry,也就是没有通过指针产生Entry链时,此时的HashMap具有最好的性能。当程序通过key取出对应value时,系统只要先计算出该key的hashCode()返回值,再根据该hashCode返回值找出该key在table数组中的索引,然后取出该索引处的Entry,最后返回该key对应的value即可。HashMap类的get(K key)方法代码如清单3-50所示。


代码清单3-50 HashMap的get方法源代码


public V get(Object key)   

{   

 // 如果 key 是 null,调用 getForNullKey 取出对应的 value   

 if (key == null)   

     return getForNullKey();   

 // 根据该 key 的 hashCode 值计算它的 hash 码

 int hash = hash(key.hashCode());   

 // 直接取出 table 数组中指定索引处的值,

 for (Entry<K,V> e = table[indexFor(hash, table.length)];   

     e != null;   

     // 搜索该 Entry 链的下一个 Entr   

     e = e.next)         // ①

 {   

     Object k;   

     // 如果该 Entry 的 key 与被搜索 key 相同

     if (e.hash == hash && ((k = e.key) == key   

         || key.equals(k)))   

         return e.value;   

 }   

 return null;   

}


如果HashMap的每个bucket里只有一个Entry时,HashMap可以根据索引、快速地取出该 bucket里的Entry。在发生“Hash冲突”的情况下,单个bucket里存储的不是一个Entry,而是一个Entry链,系统只能必须按顺序遍历每个Entry,直到找到想搜索的Entry为止——如果恰好要搜索的Entry位于该Entry链的最末端(该Entry是最早放入该bucket中),那系统必须循环到最后才能找到该元素。


归纳起来简单地说,HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。HashMap底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个 Entry对象时,会根据Hash算法来决定其存储位置;当需要取出一个Entry时,也会根据Hash算法找到其存储位置,直接取出该Entry。


当创建HashMap时,有一个默认的负载因子(load factor),其默认值为0.75,这是时间和空间成本上一种折衷,增大负载因子可以减少Hash表(就是那个Entry数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap的get()与put()方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。


综上所述,我们可以在创建HashMap时根据实际需要适当地调整load factor的值,如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子,如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子。通常情况下,程序员无需改变负载因子的值。


如果开始就知道HashMap会保存多个key-value对,可以在创建时就使用较大的初始化容量,如果HashMap中Entry的数量一直不会超过极限容量(capacity * load factor),HashMap就无需调用resize()方法重新分配table数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为capacity的Entry数组),因此创建HashMap 时初始化容量设置也需要小心对待。


从上面的源代码分析可以得出,HashMap的高性能需要以下3点来提供保证。

(1)提供高效的Hash算法;

此外,能够不用Map就不要用了吧,当我们想遍历一个用键值对形式保存的Map时,下面两种方式其实效率都不高,如清单3-51所示。


代码清单3-51 map循环代码


for (K key : map.keySet()) {

    V value : map.get(key);

}

for (Entry<K, V> entry : map.entrySet()) {

    K key = entry.getKey();

    V value = entry.getValue();

}



麦克叔叔

每晚十点说



以上是关于HashMap使用经验(下)的主要内容,如果未能解决你的问题,请参考以下文章

HashMap优化使用

这五年的工作经验是假的吧?怎么连这些HashMap问题都回答不出来

老程序员分享一下JAVA如何处理表格的经验,真的不要太爽

4年经验Java面试,题目及答案

mysql基本数据类型,附带学习经验

5年crud经验,转疯了!