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使用经验(下)的主要内容,如果未能解决你的问题,请参考以下文章