HashMap工作原理

HashMap存储的实现(put()方法)

HashMap采用了一种所谓的“Hash算法”来决定每个元素的存储位置。

当程序执行map.put("语文",80.0)时,系统将调用"语文"(即Key)的hashCode()方法得到其hashCode值---每个java对象都有hashCode()方法,都可以通过该方法获得它的hashCode值。得到这个对象的hashCode值之后,系统根据hashCode值来决定 该元素的存储位置。

我们可以看HashMap类的put(K key,V value)方法的源代码:

  1. public V put(K key, V value)   
  2. {   
  3.     // 如果 key 为 null,调用 putForNullKey 方法进行处理  
  4.     if (key == null)   
  5.         return putForNullKey(value);   
  6.     // 根据 key 的 keyCode 计算 Hash 值  
  7.     int hash = hash(key.hashCode());   
  8.     // 搜索指定 hash 值在对应 table 中的索引  
  9.     int i = indexFor(hash, table.length);  
  10.     // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素  
  11.     for (Entry<K,V> e = table[i]; e != null; e = e.next)   
  12.     {   
  13.         Object k;   
  14.         // 找到指定 key 与需要放入的 key 相等(hash 值相同  
  15.         // 通过 equals 比较放回 true)  
  16.         if (e.hash == hash && ((k = e.key) == key   
  17.             || key.equals(k)))   
  18.         {   
  19.             V oldValue = e.value;   
  20.             e.value = value;   
  21.             e.recordAccess(this);   
  22.             return oldValue;   
  23.         }   
  24.     }   
  25.     // 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry   
  26.     modCount++;   
  27.     // 将 key、value 添加到 i 索引处  
  28.     addEntry(hash, key, value, i);   
  29.     return null;   
  30. }  
上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:hash(),这个方法是一个纯粹的数学计算,其方法如下:

static int hash(int h) 
{ 
    h ^= (h >>> 20) ^ (h >>> 12); 
    return h ^ (h >>> 7) ^ (h >>> 4); 
}

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

图 1. HashMap 的存储示意
图 1. HashMap 的存储示意

key相同的则产生链。


HashMap 的读取实现()

当 HashMap 的每个 bucket(数组a[i]) 里存储的 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 值<span style="color:#ff0000;">计算它的 hash 码  第一步</span>
	 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 与被搜索 <span style="color:#ff0000;">key 相同  第二步</span>
		 if (e.hash == hash && ((k = e.key) == key 
			 || <span style="color:#ff0000;">key.equals(k))) </span>
			 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 之所以能快速存、取它所包含的 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 时初始化容量设置也需要小心对待。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值