Java集合——HashMap原理及要点(二)

来源:互联网 发布:淘宝卖家怎么联系快递 编辑:程序博客网 时间:2024/05/21 10:16

前言

在HashMap原理及要点(一)中,提及了这么两个重要的基本概念:

  1. 初始容量(initialCapacity,整型数据)
  2. 加载因子(也称为负载因子, loadFactor,浮点类型数据)

这篇文章,将对这两个概念作出详细说明。

概述

  对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。
  无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。如图 1 所示:
这里写图片描述
  图中,table数组的长度,就是HashMap底层数组的容量,而每一个数组元素存储的位置被称为“桶(bucket)”,每个桶只存一个Entry对象,而Entry对象可以包含指向下一个Entry对象的变量,会形成链表,如上图所示。
  因此,HashMap中的Bucket里可以只有一个Entry对象,也可以有一个Entry链。
  

HashMap 的读取实现

  当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry ——也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:
  1、当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,
  2、再根据该 hashCode 返回值找出该 key 在 table 数组中的索引,
  3、然后取出该索引处的 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 中),那系统必须循环到最后才能找到该元素。
  这里需要注意的是,在存储过程中,
  1、如果新添加的Entry的Key存在hash冲突,则会比较Key值是否和已存在的Entry中的Key值相等,
    1.1若相等,则会用新添加的Value值覆盖旧的Value值,
    1.2若Key不相等,则新添加的Entry值将和原有的Entry形成链,且新的Entry位于头部。

  归纳起来简单地说,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 时初始化容量设置也需要小心对待。

  综上所述,对于初始容量,其实就是代表着上图中,HashMap里table数组的初始长度
  而负载因子,则是代表table中所有桶里,全部Entry数量占允许的Entry链最大数量百分比。即
  容量极限(threshold)/实际容量(capacity)=键值最高对数/实际容量