java 中的 HashMap

一、HashMap 介绍

  • HashMap 是 Map 接口使用频率最高的实现类。
  • 允许使用 null 键 和 null 值。
  • 所有的 key 构成的集合是 Set:无序的、不可重复的。key 所在的类要重写:equals() 和 hashCode()。
  • 所有的 value 构成的集合是 Collection:无序的、可以重复的。value 所在的类要重写:equals()。
  • 一个 key-value 构成一个 entry,entry 构成的集合是 Set:无序的、不可重复的。
  • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法 返回 true,hashCode 值也相等。
  • HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true。

二、HashMap 的存储结构

jdk7 及以前版本:是数组 + 链表结构(即为链地址法)
java 中的 HashMap

jdk8以后:数组 + 链表 + 红黑树
java 中的 HashMap

三、jdk1.8 之前 HashMap 存储结构详解

1、介绍

  • HashMap 的内部存储结构其实是数组和链表的结合。当实例化一个 HashMap 时, 系统会创建一个长度为 Capacity 的 Entry 数组,这个长度在哈希表中被称为容量 (Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个 bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
  • 每个 bucket 中存储一个元素,即一个 Entry 对象,每一个 Entry 对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。 而且新添加的元素作为链表的head。

2、添加元素的过程
向 HashMap 中添加 entry1(key,value),需要首先计算 entry1 中 key 的哈希值(根据 key 所在类的hashCode() 计算得到),此哈希值经过处理以后,得到在底层 Entry[] 数组中要存储的位置 i。如果位置 i 上没有元素,则 entry1 直接添加成功。如果位置 i 上已经存在 entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较 entry1 中 key 和其他的 entry。如果彼此 hash 值相同,则直接添加成功。如果 hash 值不同,继续比较二者是否 equals。如果返回值为 true,则使用 entry1 的 value 去替换 equals 为 true 的 entry 的 value。如果遍历一遍以后,发现所有的 equals返回都为 false,则 entry1 仍可添加成功。entry1 指向原有的 entry 元素。

3、HashMap的扩容
当 HashMap 中的元素越来越多的时候,hash 冲突的机率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对 HashMap 的数组进行扩容,而在 HashMap 数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

那么 HashMap 什么时候进行扩容呢?
当 HashMap 中的元素个数超过数组大小(数组总大小 length,不是数组中个数 size) * loadFactor 时 ,就会进行数组扩容,loadFactor 的默认值 (DEFAULT_LOAD_FACTOR)为 0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为 16,那么当 HashMap 中元素个数超过 160.75=12 (这个值就是代码中的 threshold 值,也叫做临界值)的时候,就把数组的大小扩展为 216=32,即扩大一倍,然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数, 那么预设元素的个数能够有效的提高HashMap的性能。

四、jdk1.8 之后的 HashMap 存储结构详解

1、介绍

  • HashMap 的内部存储结构其实是数组+链表+树的结合。当实例化一个 HashMap 时,会初始化initialCapacity 和 loadFactor,在 put 第一对映射关系时,系统会创建一个长度为 initialCapacity 的 Node 数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为 “桶”(bucket),每个 bucket 都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

  • 每个 bucket 中存储一个元素,即一个 Node 对象,但每一个 Node 对象可以带 一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode 对象,每一个 TreeNode 对象可以有两个叶子结点 left 和 right,因此,在一个桶中,就有可能生成一个 TreeNode 树。而新添加的元素作为链表的 last,或树的叶子结点。

2、HashMap 什么时候进行扩容和树形化呢?
当 HashMap 中的元素个数超过数组大小(数组总大小 length,不是数组中个数 size) * loadFactor 时,就会进行数组扩容,loadFactor 的默认值(DEFAULT_LOAD_FACTOR)为 0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当 HashMap 中元素个数超过160.75=12(这个值就是代码中的 threshold 值,也叫做临界值)的时候,就把数组的大小扩展为 216=32,即扩大一倍,然后重新计算每个元 素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。

当 HashMap 中的其中一个链的对象个数如果达到了 8个,此时如果 capacity 没有达到 64,那么HashMap 会先扩容解决,如果已经达到了 64,那么这个链会变成树,结点类型由 Node 变成TreeNode 类型。当然,如果当映射关系被移除后, 下次 resize 方法时判断树的结点个数低于6个,也会把树再转为链表。

五、HashMap 源码中的重要常量

java 中的 HashMap

六、负载因子对 HashMap 有什么影响?

java 中的 HashMap

七、总结

JDK1.8相较于之前的变化:
1、HashMap map = new HashMap(); //默认情况下,先不创建长度为16的数组。
2、当首次调用 map.put() 时,再创建长度为 16 的数组。
3、数组为 Node 类型,在 jdk7 中称为 Entry 类型。
4、形成链表结构时,新添加的 key-value 对在链表的尾部
5、当数组指定索引位置的链表长度 > 8 时,且 map 中的数组的长度 > 64 时,此索引位置上的所有key-value 对使用红黑树进行存储。