在开始分析 HashMap 核心源码之前,本文先试着从使用的角度对其进行解读,包括 HashMap 特性、如何正确使用,以及影响 Map 性能的因素等。

HashMap特性

HashMap 是 Map 接口的一个基于哈希表实现,主要用来存储键值对数据。作为一个使用频率较高的集合类,了解它的特性有助于我们在实践中正确使用它。

首先,HashMap 允许键为 null 和值为 null。

其次,HashMap 不保证 Map 的有序性,即不保证遍历 HashMap 输出元素的顺序与其插入的顺序相同(可能存在顺序相同情况)。另外,HashMap 也不保证其顺序在一段时间内保持不变。如果需要在项目中保证 Map 有序性的话,可以使用 LinkedHashMap 代替 HashMap。

再次,HashMap 是不同步的,即 HashMap 不是线程安全的。如果多个线程同时访问同一个 HashMap 实例,并且其中至少有一个线程从结构上修改了 HashMap 实例时,会导致数据不一致的情况发生。这里的“从结构上修改”是指对 HashMap 实例进行添加或删除元素的操作,仅修改已包含的键对应的值话,就不是从结构上修改。

在需要线程安全环境中,可以在创建 HashMap 实例时使用 Collections.synchronizedMap 方法对其进行“包装”,以防止对 HashMap 的意外非同步访问:

1
Map m = Collection.synchronizedMap(new HashMap (…));

另外,也可以用 ConcurrentHashMap 代替 HashMap。

最后,HashMap 的所有“集合视图方法”返回的迭代器是快速失败(fail-fast)的。如果在创建迭代器之后,除了通过迭代器本身的 remove 方法之外,在任何时候以任何方式从结构上修改 HashMap 实例时,迭代器都会抛出 ConcurrentModificationException 异常。因此,在面对并发修改时,迭代器会快速、干净利落地失败,而不是在未来某个不确定的时间出现随意的、不确定的行为风险。

注意,迭代器的快速失败行为是无法保证的,因为一般而言,在存在非同步并发修改的情况下,不可能做出任何硬保证。快速失败的迭代器会尽最大努力抛出 ConcurrentModificationException 异常。因此,编写依赖于此异常的正确性的程序是错误的:迭代器的快速失败行为应该只用于检测错误。

HashMap性能

如果哈希函数可以把元素均匀打散到不同的哈希桶中,那么 HashMap 的基本操作(get 和 put)能达到常数级时间的性能。这意味着理想情况下,HashMap 的基本操作的时间复杂度为 O(1),执行时间不随数据规模(键值对数量)的增大而增长。

但是在真实的情况下,当你往 HashMap 中写入大量数据后,就可能发现操作有时候会变慢了。这其实是因为哈希冲突和扩容带来的性能影响。

在 HashMap 中,有两个参数——初始容量(initial capacity)和装载因子(load factor),与哈希冲突和扩容这两个性能因素有直接关系。

初始容量,顾名思义就是哈希表创建时的容量,这里的容量是指哈希表的哈希桶数。装载因子是衡量哈希表在其容量自动扩容之前允许达到多满的指标。当哈希表中的条目数超过负载因子和当前容量的乘积时,即

条目数 >= 哈希表容量 × 装载因子

将对哈希表进行重新哈希(即重新构建内部数据结构),使哈希表的桶数为原来的两倍。

一般来说,默认的装载因子(为 0.75)可以在时间和空间的开销上做到很好的权衡。较大的值虽然可以减少空间开销,但会增加查找成本(反映在 HashMap 的大多数操作中,包括 get 和 put 操作)。

在设置初始容量时,应考虑映射中的预期条目数及其装载因子,以尽量减少重新哈希操作的次数。如果初始容量大于最大条目数除以负载因子,则不会发生重新哈希操作。

如果有大量的键值对数据要存储在 HashMap 实例中,那么在创建时就指定一个足够大的容量来存储这些数据,会比随着表增长的需要而自动执行重新哈希更加高效。

注意,如果使用大量的具有相同哈希码(hashCode())的键,无疑会降低哈希表的性能。为了改善影响,当键是可比较时,可以使用键之间的比较顺序来帮助打破联系。

另外,遍历 HashMap 所需的时间与 HashMap 实例的“容量”(指哈希桶数量)加上其大小(指键值映射数量)成正比。所以,如果你看重遍历 HashMap 的性能,那么不要把它初始容量设置得太大或把装载因子设置得太小就非常重要了。

TreeMap, Hashtable 和 HashMap

TreeMap 是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断。

Hashtable 大致相当于 HashMap ,只是 HashMap 是非线程安全的并且允许空值。

需要注意的是,Hashtable 是遗留类,不建议在项目中使用。在非线程安全环境中,可以直接用 HashMap 代替 Hashtable;在需要线程安全环境中,可以用 ConcurrentHashMap 代替 Hashtable,因为 ConcurrentHashMap 引入了分段锁,并发性更好。

小结

本文概述了 HashMap 允许键为 null 和值为 null、不保证有序性、非线程安全、迭代器快速失败(fail-over)等特性,影响 HashMap 性能的参数,以及在实践中应该注意的地方,同时在文末简单对比了 TreeMap, Hashtable 和 HashMap 有什么不同。下一篇文章开始将对 HashMap 核心功能源码进行解读。