您的位置:首页 > 其它

集合系列--HashMap实现详解

2013-05-30 21:17 567 查看
1、HashMap特点

              HashMap是基于哈希表的map接口的非同步实现,key不可重复,但value可以,其中key和value都可以为空,且此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

 

2、HashMap的数据结构

hashmap 是一个链表散列的数据结构,即数组和链表的结合体,对于是怎样结合,我们首先看一下hashmap 的源码:

 

/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;

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


当创建一个HashMap是,系统会自动的创建一个table数组来保存HashMap中的键值对,并持有指向下一个元素的引用,这样就构成了链表,我们看一下HashMap的存储实现:当系统调用HashMap的put方法保存元素 eg:

hashMap.put("语文",80.0);


当程序执行这句话时,系统将调用“语文“的hashCode()方法得到其hashCode值,然后根据hashCode的值来决定该元素的存储位置,我们来看一下HashMap的put实现:

 

public V put(K key, V value) {
// HashMap允许存放null键和null值。
// 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey(value);
// 根据key的keyCode重新计算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在对应table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
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;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
}


从put源码可以看出,将一个key-value存入HashMap中时,会先根据key的hashCode()的返回值决定entry的存储位置,如果两个entry的hashCode()相同,那么他们的存储位置相同,然后比较两个entry是否equals,如果equals的结果返回true,则新添加的entry的value值将会覆盖原entry的value值,但key将不变,如果返回false,新添加的entry将会指向原entry形成链表,新添加的entry将会位于entry链的头部,其中addEntry方法根据计算出的hash值将entry保存在table的i索引出,我们来看一下addEntry(hash,key,value,i)的源码:

void addEntry(int hash, K key, V value, int bucketIndex) {
// 获取指定 bucketIndex 索引处的 Entry
Entry<K,V> e = table[bucketIndex];
// 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 如果 Map 中的 key-value 对的数量超过了极限
if (size++ >= threshold)
// 把 table 对象的长度扩充到原来的2倍。
resize(2 * table.length);
}


从源码可以看出,系统总是将新加入的Entry对象放在table数组的bucketIndex索引处,如果该索引出没有对象,则新放入的Entry对象将指向该索引,如果该索引出已经有其他Entry对象,则新添加的Entry将指向原来的Entry形成Entry链如图:



 

 

3、HashMap的性能

 首先看一下HashMap的构造方法:

HashMap();构造一个厨师容量为16,负载因子为0.75的HashMap

HashMap(int initialCapacity);构建一个初始容量为initialCapacity,负载因子为0.75的HashMap

HashMap(int initialCapacity,float loadFactor);构建一个初始容量为initialCapacity,负载因子为loadFactor的HashMap

我们来看一下第三个构造函数的源码:

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);

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

this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}




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

可以看出,HashMap的实际容量并不一定是指定的initialcapacity,他们只有当所指定的大小为2的n次幂的时候才相等,如果所指定的大小不为2的n次幂时,实际容量为大于所指定容量的最小的2的n次幂值。比如所指定的容量大小为5,那么实际容量为大于5的最小的2的n次幂值为8,所以HashMap的实际容量为8,。

对于默认的负载因子为0.75,这是时间和空间成本上的一种折中,增大负载因子可以减少hash表(Entry数组)所占用的内存空间,但会增加查询数据的时间开销,减少负载因子会提高数据查询性能,但会增加hash表所占用的内存空间。

在HashMap中通过threshold字段来判断HashMap的最大容量

threshold = (int)(capacity * loadFactor);


threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,将数组扩充为原来的一倍:

if (size++ >= threshold)
resize(2 * table.length);  

 

比如说:原容量为16,负载因子为0.75,那么

threshold = 16*0.75 = 12,也就是说,当HashMap中元素个数超过12时,就把数组扩充为16*2 = 32,这样是比较耗性能的,他得重新计算各个元素在数组中的位置。

 

4、HashMap的读取实现

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

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实现原理
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: